savon 2.18.0 → 2.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b824b46b6baa9ec754839a85d8f4225b62ba562b4d060143c6b02cd41e4880f8
4
- data.tar.gz: fbaf6f7ed2607e3c24d356c6ffb282f089b1426fc89b8fdcde5c01918c657e9a
3
+ metadata.gz: cec32bb7e6cf1477f3d93ab0befe29d05386220c73c46d62ab747bf822f591f3
4
+ data.tar.gz: 2c18c7678d283d3f8e29cb18bdce837a515afa0615cfe254e3702063caebee93
5
5
  SHA512:
6
- metadata.gz: efd307c9e7dffa95ddccedf76b3bc675f27c3974d8742722ff2ebfb50b173ca687dd83a6a91fd97f637a56e5057e928d0dfa9adbcdfb0efb6c5c639215ba620f
7
- data.tar.gz: 5170029557b974cb66c5adb409d339c3a2d044ee0427aca330e275878afbfbaf59d0d4f5c51bdd63fd9a68d1c4ec8720e617ac3ce09ad64cb59645d2872a948e
6
+ metadata.gz: ae62d330b6fb3ade1fd4a78a4cbec0a2e79f3511dc9b60ba234939f02c489588d9651953d3adbf4326aab07d0765be9eda8dc1c5aa9db0fc21a5093087660279
7
+ data.tar.gz: 6ad397a186b317a3e9a1ba9caf125b54476dccaffdc8fac599653824e32686fa772075dcd504da037545f5d1f60bbdccad3d89be279da5cb840832e9dccca9c4
data/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.18.1] - 2026-11-06
9
+
10
+ ### Security
11
+
12
+ * The outbound `SOAPAction` header now rejects control characters to prevent HTTP header injection ([GHSA-57m8-w3r7-5p3w](https://github.com/savonrb/savon/security/advisories/GHSA-57m8-w3r7-5p3w)). A crafted WSDL can encode a carriage return or line feed in its `soapAction` attribute as the character references `
` and `
`, which survive XML attribute normalization and reach the header as real control characters. The httpclient adapter sent such a header unchecked, so it split on the wire and let an attacker inject further request headers. HTTPI picks httpclient automatically whenever the gem is installed, so the default configuration was affected. Both transports now validate the outgoing headers before the request is sent. A `SOAPAction` containing CR, LF, or NUL raises `Savon::InvalidHeaderError`, whether it comes from the WSDL, the `:soap_action` option, or the `:headers` option under any key spelling. With the net/http adapter, CR and LF used to raise net/http's own `ArgumentError`. A leading or trailing newline in a `:headers` value was stripped silently, and NUL was sent as-is. All of these now raise `Savon::InvalidHeaderError`. Thanks to @SDX442 for securely disclosing this.
13
+
8
14
  ## [2.18.0] - 2026-11-05
9
15
 
10
16
  ### Added
@@ -15,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
21
 
16
22
  ### Security
17
23
 
18
- * `Savon::Model.operations` and `.all_operations` no longer rely on a hand-maintained list of reserved names. The guard now derives its denylist from the model's own ancestor chains — every method instances respond to plus every method the class object responds to — so a WSDL operation can never shadow an existing method. This closes the `class`/`public_send` bypass (unbounded recursion via the generated `self.class.public_send(...)` bodies, hijacked reflective dispatch) and the whole class of follow-on reports (`new`, `freeze`, `send`, …) by construction instead of one advisory at a time. Re-registering an already-defined operation is still allowed; anything else that collides raises `ArgumentError`. Call such operations explicitly through `model.client.call(:operation, ...)`.
24
+ * `Savon::Model.operations` and `.all_operations` no longer rely on a hand-maintained list of reserved names ([GHSA-hjvq-xxqv-7jxh](https://github.com/savonrb/savon/security/advisories/GHSA-hjvq-xxqv-7jxh)). The guard now derives its denylist from the model's own ancestor chains — every method instances respond to plus every method the class object responds to — so a WSDL operation can never shadow an existing method. This closes the `class`/`public_send` bypass (unbounded recursion via the generated `self.class.public_send(...)` bodies, hijacked reflective dispatch) and the whole class of follow-on reports (`new`, `freeze`, `send`, …) by construction instead of one advisory at a time. Re-registering an already-defined operation is still allowed; anything else that collides raises `ArgumentError`. Call such operations explicitly through `model.client.call(:operation, ...)`. Thanks to @manus-pi for securely disclosing this.
19
25
 
20
26
  ### Fixed
21
27
 
@@ -37,7 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
37
43
 
38
44
  ### Security
39
45
 
40
- * `Savon::Model.operations` and `.all_operations` now reject names that normalize to reserved model methods with `ArgumentError`, before defining any methods. This prevents a WSDL operation named `client` from overwriting the client accessor and causing unbounded recursion. Call operations with reserved names explicitly through `model.client.call(:client, ...)`.
46
+ * `Savon::Model.operations` and `.all_operations` now reject names that normalize to reserved model methods with `ArgumentError`, before defining any methods ([GHSA-x99v-rwvj-g9p5](https://github.com/savonrb/savon/security/advisories/GHSA-x99v-rwvj-g9p5)). This prevents a WSDL operation named `client` from overwriting the client accessor and causing unbounded recursion. Call operations with reserved names explicitly through `model.client.call(:client, ...)`. Thanks to @arpitjain099 for securely disclosing this.
41
47
 
42
48
  ## [2.17.4] - 2026-07-03
43
49
 
@@ -1296,6 +1302,9 @@ Pay attention to the following list and read the updated Wiki: http://wiki.githu
1296
1302
 
1297
1303
  * Complete rewrite and public release.
1298
1304
 
1305
+ [2.18.1]: https://github.com/savonrb/savon/compare/v2.18.0...v2.18.1
1306
+ [2.18.0]: https://github.com/savonrb/savon/compare/v2.17.5...v2.18.0
1307
+ [2.17.5]: https://github.com/savonrb/savon/compare/v2.17.4...v2.17.5
1299
1308
  [2.17.4]: https://github.com/savonrb/savon/compare/v2.17.3...v2.17.4
1300
1309
  [2.17.3]: https://github.com/savonrb/savon/compare/v2.17.2...v2.17.3
1301
1310
  [2.17.2]: https://github.com/savonrb/savon/compare/v2.17.1...v2.17.2
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "savon/transport/logging"
4
4
  require "savon/transport/response"
5
+ require "savon/transport/header_validation"
5
6
 
6
7
  module Savon
7
8
  module Transport
@@ -15,6 +16,7 @@ module Savon
15
16
  # the caller's responsibility via client.faraday before any call is made.
16
17
  class Faraday
17
18
  include Logging
19
+ include HeaderValidation
18
20
 
19
21
  # @param connection [Faraday::Connection] the memoized connection from client.faraday
20
22
  # @param globals [Savon::GlobalOptions] the client-level options
@@ -79,6 +81,8 @@ module Savon
79
81
  # soap_headers are lowest priority
80
82
  soap_headers.each do |k, v| headers[k] ||= v end
81
83
 
84
+ validate_soap_action_header!(headers)
85
+
82
86
  cookie_header = format_cookies(locals[:cookies])
83
87
  headers["Cookie"] = cookie_header if cookie_header
84
88
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Savon
4
+ module Transport
5
+ # Validates outbound HTTP header values before a transport sends them.
6
+ #
7
+ # A value that carries control characters cannot be sent safely.
8
+ # A carriage return or line feed splits the header on the wire,
9
+ # which lets an attacker inject further request headers.
10
+ # A header value can be assembled from input the caller does not fully control,
11
+ # so it is checked here rather than trusted where it is built.
12
+ #
13
+ # The check runs at the transport layer on the fully merged header set,
14
+ # so it covers every source of a value alike. It is the policing half of
15
+ # a deliberate split: the request headers are built elsewhere, this module
16
+ # decides whether they may be sent.
17
+ #
18
+ # RFC 9110 §5.5 (field values)
19
+ module HeaderValidation
20
+ # Control characters that must never appear in an HTTP header value.
21
+ # A carriage return or line feed splits the header, a NUL truncates it.
22
+ ILLEGAL_HEADER_CHARACTERS = /[\r\n\0]/
23
+ private_constant :ILLEGAL_HEADER_CHARACTERS
24
+
25
+ private
26
+
27
+ # Validates every SOAPAction header and raises if one is unsafe.
28
+ #
29
+ # The SOAPAction value can come from untrusted WSDL input, which may encode
30
+ # a carriage return or line feed as the character references +
+ and +
+.
31
+ # These survive XML attribute-value normalization and reach the header as real
32
+ # control characters.
33
+ #
34
+ # The merged headers are a plain Hash, so a caller can add a SOAPAction under
35
+ # a Symbol or a differently cased key next to the default String key.
36
+ # Some adapters send both, so every key that names SOAPAction is checked,
37
+ # matched case-insensitively.
38
+ #
39
+ # Pure validation. The value is never trimmed, stripped, or encoded.
40
+ # A header that cannot be sent safely is rejected, not repaired.
41
+ # Leading, interior, and trailing control characters all fail.
42
+ # The value is checked as raw bytes, so a non-String value or a string
43
+ # with an invalid encoding never raises here.
44
+ # The offending value is included in the message because it's not a secret.
45
+ #
46
+ # SOAP 1.1 §6.1.1 (SOAPAction HTTP header)
47
+ #
48
+ # @param headers [Hash{String, Symbol => Object}] the merged outbound HTTP headers
49
+ # @return [void]
50
+ # @raise [Savon::InvalidHeaderError] when a SOAPAction contains CR, LF, or NUL
51
+ def validate_soap_action_header!(headers)
52
+ headers.each do |key, value|
53
+ next unless value && key.to_s.casecmp?("SOAPAction")
54
+ next unless ILLEGAL_HEADER_CHARACTERS.match?(value.to_s.b)
55
+
56
+ raise InvalidHeaderError, "The SOAPAction header contains illegal control " \
57
+ "characters (CR, LF, or NUL): #{value.inspect}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -3,6 +3,7 @@
3
3
  require "httpi"
4
4
  require "savon/transport/logging"
5
5
  require "savon/transport/response"
6
+ require "savon/transport/header_validation"
6
7
 
7
8
  module Savon
8
9
  module Transport
@@ -14,6 +15,7 @@ module Savon
14
15
  # * request execution
15
16
  class HTTPI
16
17
  include Logging
18
+ include HeaderValidation
17
19
 
18
20
  # @param globals [Savon::GlobalOptions] the client-level options
19
21
  def initialize(globals)
@@ -63,6 +65,8 @@ module Savon
63
65
  # soap_headers are lowest priority
64
66
  soap_headers.each do |k, v| headers[k] ||= v end
65
67
 
68
+ validate_soap_action_header!(headers)
69
+
66
70
  http_request = ::HTTPI::Request.new
67
71
  http_request.url = url
68
72
  http_request.body = body
data/lib/savon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Savon
4
- VERSION = '2.18.0'
4
+ VERSION = '2.18.1'
5
5
  end
data/lib/savon.rb CHANGED
@@ -7,6 +7,10 @@ module Savon
7
7
  UnknownOperationError = Class.new(Error)
8
8
  InvalidResponseError = Class.new(Error)
9
9
 
10
+ # Raised when a resolved HTTP header value cannot be sent safely,
11
+ # for example a SOAPAction carrying control characters from a crafted WSDL.
12
+ InvalidHeaderError = Class.new(Error)
13
+
10
14
  def self.client(globals = {}, &block)
11
15
  Client.new(globals, &block)
12
16
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: savon
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.18.0
4
+ version: 2.18.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Harrington
@@ -291,6 +291,7 @@ files:
291
291
  - lib/savon/soap_fault.rb
292
292
  - lib/savon/string_utils.rb
293
293
  - lib/savon/transport/faraday.rb
294
+ - lib/savon/transport/header_validation.rb
294
295
  - lib/savon/transport/httpi.rb
295
296
  - lib/savon/transport/logging.rb
296
297
  - lib/savon/transport/response.rb