FlawOpen

flawopen.com/Insecure Deserialization/Ruby YAML

Is YAML.load safe on untrusted input?

CWE-502: Deserialization of Untrusted DataReference page
Short answer

Check the Psych version actually loaded by the application: Psych 4, bundled with Ruby 3.1, changed YAML.load to use safe deserialization by default. Earlier Psych versions used unsafe object construction; Psych can also be upgraded independently of Ruby. YAML.unsafe_load and Marshal.load deserialize Ruby objects and must never receive untrusted input.

VULNERABLE
# Psych < 4: YAML.load used unsafe
# object construction by default.
YAML.load(params[:config])

# Every Psych version: explicitly unsafe.
YAML.unsafe_load(user_input)
Psych.unsafe_load(user_input)

# Marshal has no restricted safe-load API.
Marshal.load(cookie_value)
FIXED
# Explicit restricted types; keep this list minimal.
require "yaml"
require "date"

data = YAML.safe_load(
  input,
  permitted_classes: [Date],
  permitted_symbols: [],
  aliases: false
)

# For ordinary external data, prefer JSON and
# validate its shape, values, and input size.
data = JSON.parse(input)

Why YAML is dangerous at all

YAML has tags that Psych can map to Ruby objects. Under an unsafe load, a tagged document can request construction of a class, with values supplied for its serialized state; impact depends on available classes and their behavior. This is the basis of deserialization gadget chains. Use safe loading for untrusted YAML, and validate the resulting data separately. The same broad risk applies to Python pickle, PHP unserialize, and Ruby Marshal.load.

Rails and other applications may load many dependencies, increasing the set of classes and behaviors available to an unsafe deserialization chain. This is a deployment-specific condition, not a property of every Rails application or every YAML document.

Why the version change caused confusion

Psych 4 changed YAML.load to use safe loading by default and introduced unsafe_load for the earlier object-construction behavior. Ruby 3.1 bundled Psych 4, but applications can select a different Psych gem version; audit the resolved dependency and runtime, not only the Ruby version. Because safe loading can reject application-specific classes or aliases, teams should migrate with a minimal explicit allowlist instead of switching to unsafe_load.

If an upgrade leads to unsafe_load, treat that call as a security finding whenever input can be influenced outside the trusted deployment boundary. Prefer safe_load with the smallest required permitted_classes list; do not broaden it merely to silence compatibility errors.

FAQ

Is YAML.safe_load completely safe?

Psych.safe_load restricts deserialized classes to its permitted set (basic scalar and collection types by default) and rejects aliases by default. That does not validate application-level shape, size, or business rules. Permit only classes the format truly requires, and separately bound input size and validate the returned data. Some YAML implementations and workflows have had alias-expansion denial-of-service issues; CVE-2024-35221 documents such a case in RubyGems metadata parsing. It is not evidence that every Psych safe_load call is vulnerable.

What about loading my own config files?

A file shipped inside a trusted, integrity-protected application artifact has a different provenance from an upload or tenant-controlled file. Even for internal configuration, prefer safe_load and allow only required classes. Use unsafe_load only when the data is fixed and demonstrably trusted; never use it for user- or request-influenced content.

Is Marshal ever acceptable?

Ruby documents that Marshal.load can deserialize almost any class loaded in the process and may lead to remote code execution for untrusted data. It has no restricted safe mode or class allowlist. Keep Marshal data inside a trusted boundary and never deserialize attacker-influenced bytes.

A separate Ruby risk: Marshal.load

Separate from YAML, Ruby documents that Marshal.load can rebuild almost any class loaded in the process and may lead to remote code execution when the data is untrusted. Keep Marshal data inside a trusted boundary; never unmarshal user-supplied or attacker-influenced bytes.

References

See also Insecure Deserialization: Risks and Remediation .NET BinaryFormatter Security: Risks and Migration Java ObjectInputStream Security: Safer Deserialization Newtonsoft.Json TypeNameHandling: Safe Configuration? PHP unserialize() and Untrusted Input Python pickle.load() Security: Untrusted Data Risks