Vulnerability DatabaseGHSA-hjvq-xxqv-7jxh

GHSA-hjvq-xxqv-7jxh: 
Ruby vulnerability analysis and mitigation

Summary

Savon::Model dynamically generates instance and class methods named after the SOAP operations advertised by a WSDL document. A guard in Savon::Model#operation_method_name (lib/savon/model.rb) rejects operation names that collide with the mixin's own methods plus a short hard-coded list (client, global, raise_initialization_error!). That guard does not reserve the inherited Object#class or Kernel#public_send methods that every generated instance method dereferences. A WSDL advertising an operation named class therefore passes the guard, defines a method that shadows Object#class, and turns each generated instance operation into unbounded self-recursion that raises SystemStackError, disabling the model's entire SOAP client surface. No authentication is required; the attacker only needs to influence the WSDL operation names the model loads.

Root Cause

The security invariant is that no generated operation method may shadow a method that the generated method bodies themselves rely on. Each instance operation body is:

def define_instance_operation(method_name)
  instance_operation_module.define_method(method_name) do |locals = {}|
    self.class.public_send(method_name, locals)   # depends on Object#class and Kernel#public_send
  end
end

The guard that is supposed to enforce the invariant only considers Savon::Model's own methods and three explicit accessors:

def operation_method_name(operation)
  method_name = StringUtils.snakecase(operation.to_s).to_sym
  reserved_methods = Model.instance_methods(false) + Model.private_instance_methods(false) +
                     %i[client global raise_initialization_error!]
  raise ArgumentError, "... conflicts with reserved Savon::Model method ..." if reserved_methods.include?(method_name)
  method_name
end

The reserved set never contains the inherited :class or :public_send. When an operation name normalizes to :class, the generated instance method is defined in a module that is included ahead of Object in the ancestor chain, so self.class inside the body dispatches back into the generated method instead of Object#class, producing infinite recursion. The sibling collision public_send breaks every instance body's dispatch call for the same reason.

Impact

Prerequisites: the application uses the documented Savon::Model mixin and loads operations from a WSDL whose operation names the attacker can influence (for example a remote or third-party WSDL, or a service description an attacker can register). No authentication, special configuration, or user interaction is required, and the operation set is normally loaded automatically via all_operations. Demonstrated capability: a single operation named class causes every generated instance operation on the model to raise SystemStackError when invoked, because each body begins with self.class. This is a complete availability loss for the model's SOAP client surface within the hosting process. A plausible secondary effect is that reflective calls such as ModelClass.class become unintended outbound SOAP dispatches rather than returning the class object; this follows directly from the method shadowing and is noted as plausible, not observed as an external exploit.

Proof of Concept

Dependencies: Ruby 3.2.x. The PoC loads the verbatim released lib/savon/model.rb and
lib/savon/string_utils.rb. Only Savon::Client is stubbed to advertise a WSDL operation
named "class"; the stub is never reached because the recursion occurs before any client call.

Steps:

  1. Extend a plain class with Savon::Model and load operations from a WSDL that advertises an operation named class (simulated by a client stub returning [:class, :get_user]).
  2. Call all_operations to generate the operation methods.
  3. Invoke any instance operation.
require "savon/string_utils"
module Savon
  class InitializationError < StandardError; end
  class Client
    def initialize(g = {}); end
    def operations; [:class, :get_user]; end
    def call(op, locals = {}); [[op.to_s, locals]]; end
  end
  module Model; end
end
load "lib/savon/model.rb"
klass = Class.new
klass.extend(Savon::Model)
p klass.all_operations            # => [:class, :get_user]  (name "class" accepted)
inst = klass.new
inst.class                        # => raises SystemStackError
inst.get_user                     # => raises SystemStackError (every instance op bricked)
all_operations accepted WSDL op 'class' (fix bypassed): true
inst.class -> SystemStackError (unbounded recursion / DoS)
inst.get_user -> SystemStackError (every instance op bricked)
[control] op 'client' still BLOCKED: SOAP operation "client" conflicts with reserved Savon::Model method :client
recursion backtrace frame: lib/savon/model.rb:41:in 'block in define_instance_operation'

Attack Chain

  1. Exposure: An application extends a class with Savon::Model and calls all_operations, which loads operation names from the configured WSDL (operations(*client.operations)).
  2. Control: The attacker influences a WSDL operation name. class, Class, and CLASS all normalize through StringUtils.snakecase to the string "class" and then to the symbol :class.
  3. Path: all_operations passes each name to operation_method_name, then operations calls define_class_operation and define_instance_operation for the surviving name.
  4. Guard: The reserved-name check in operation_method_name is the only guard. It omits the inherited :class and :public_send, so :class passes.
  5. Primitive: define_instance_operation(:class) defines :class on the instance operation module, which is included ahead of Object, shadowing Object#class. The generated body self.class.public_send(:class, locals) re-dispatches into itself.
  6. Result: Invoking any instance operation recurses without bound and raises SystemStackError, disabling the model's SOAP operation methods for the process.

Bypass Evidence

The relevant prior fix is the reserved-name guard added in Savon::Model#operation_method_name (published as GHSA-x99v-rwvj-g9p5, the CVE-2026-53510 recursion advisory), which prevents an operation named client from overwriting the internal client accessor. That guard is present and effective for its original input: an operation named client still raises ArgumentError on the released 2.17.5 artifact. The guard is incomplete. It reserves only Savon::Model's own methods and the literals client, global, and raise_initialization_error!. It does not reserve the inherited Object#class or Kernel#public_send that the generated instance bodies dereference. Verified against the released source:

  • git show v2.17.5:lib/savon/model.rb shows the identical incomplete reserved list; the fix commit and HEAD carry the same code.
  • The computed reserved set is [:all_operations, :class_operation_module, :client, :define_class_operation, :define_instance_operation, :global, :instance_operation_module, :operation_method_name, :operations, :raise_initialization_error!, :setup]; membership tests return false for both :class and :public_send.
  • snakecase("class") == "class", snakecase("Class") == "class", snakecase("CLASS") == "class", all yielding :class. Disproof attempts and outcomes: (a) that snakecase mangles the name, disproven, :class survives; (b) that the generated override calls Object#class rather than itself, disproven, the runtime backtrace originates at lib/savon/model.rb:41 and recursion is confirmed; (c) that the recursion lives only in stub code, disproven, the recursion occurs before any client call and the client stub is never reached; (d) that the fix already blocks this input, disproven, client is blocked while class is accepted. A leading-dot name .class normalizes to _class (a safe name) and is not part of the bypass set; the valid aliases are class, Class, and CLASS.

Affected Versions

  • Ecosystem: RubyGems
  • Package: savon
  • Confirmed affected range: <= 2.17.5
  • Latest release checked: 2.17.5 (RubyGems; repository HEAD 5a70d2f = v2.17.5 plus 8 commits carries the identical guard)
  • Fix status: not fixed (no published release blocks the class / public_send vector) The incomplete guard shipped with the recursion fix in 2.17.5, the latest published release, which is affected. Releases earlier than 2.17.5 have no reserved-name check at all and are equally or more exposed to an operation named class, so the lower bound is left open. No published release fixes the class / public_send vector, and the current development head still carries the same incomplete guard, so no fixed version is inferred from the development branch.

Suggested Fix

Enforce the invariant that a generated operation name may not shadow any method the generated bodies depend on. In operation_method_name, extend the reserved set beyond Savon::Model's own methods to also reject names that collide with the inherited methods the generated instance and class bodies dereference, at minimum :class and :public_send, and defensively :send and :__send__. A more robust approach validates the candidate name against the actual receiver's ancestor method table (for example via respond_to? or method_defined? against the target module and its ancestors) rather than only Savon::Model's own methods, so future body dependencies are covered automatically. As an interim mitigation without a code change, deployments can validate or allowlist WSDL operation names before loading them into a model, rejecting reserved Ruby object method names such as class and public_send. Reported by zx (GitHub: @manus-pi).


Source: NVD

Related Ruby vulnerabilities:

CVE ID

Severity

Score

Technologies

Component name

CISA KEV exploit

Has fix

Published date

GHSA-hjvq-xxqv-7jxhMEDIUM5.9
  • Ruby logoRuby
  • savon
NoYesNov 06, 2026
GHSA-57m8-w3r7-5p3wMEDIUM4
  • Ruby logoRuby
  • savon
NoYesNov 06, 2026
GHSA-6vgc-f9v5-8939MEDIUMN/A
  • Ruby logoRuby
  • metainspector
NoYesNov 06, 2026
GHSA-mm2p-6q2v-7ph5MEDIUMN/A
  • Ruby logoRuby
  • metainspector
NoYesNov 06, 2026
GHSA-q2gc-fpw5-3v3cMEDIUMN/A
  • JavaScript logoJavaScript
  • tina4ruby
NoYesNov 05, 2026

Free Vulnerability Assessment

Benchmark your Cloud Security Posture

Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.

Request assessment

Additional Wiz resources

Get a personalized demo

Ready to see Wiz in action?

"Best User Experience I have ever seen, provides full visibility to cloud workloads."
David EstlickCISO
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
Adam FletcherChief Security Officer
"We know that if Wiz identifies something as critical, it actually is."
Greg PoniatowskiHead of Threat and Vulnerability Management