flawopen.com/Insecure Deserialization/Ruby YAML
It depends entirely on your Ruby version, which is why this question causes so much confusion. From Ruby 3.1 (Psych 4), YAML.load is safe by default — it permits only a small set of basic types. On earlier versions it deserialized arbitrary Ruby objects and was a direct route to remote code execution. YAML.unsafe_load is never safe on untrusted input.
# Ruby < 3.1 — arbitrary object # instantiation from YAML tags YAML.load(params[:config]) # Any version — explicitly unsafe YAML.unsafe_load(user_input) Psych.unsafe_load(user_input) # Marshal is always unsafe on # untrusted data, no exceptions Marshal.load(cookie_value) # The payload shape: # --- !ruby/object:SomeClass # attr: value
# Explicit and version-independent YAML.safe_load( input, permitted_classes: [Symbol, Date], aliases: false ) # Prefer JSON for untrusted data — # it has no object-construction syntax data = JSON.parse(input) # Never round-trip session or cookie # state through Marshal or YAML. # Keep state server-side.
Unlike JSON, YAML has a type-tag syntax that maps directly onto language objects. A document containing !ruby/object:SomeClass instructs the parser to construct an instance of that class with attacker-chosen instance variables. From there the attack is a gadget chain assembled from classes already loaded in the process — the same technique used against Python's pickle and PHP's unserialize.
In a Rails application the loaded class set is very large, which historically made these chains straightforward to build.
Psych 4, bundled from Ruby 3.1, changed YAML.load to behave like safe_load and introduced unsafe_load for the old behaviour. This was a deliberate breaking change that improved the default, but it means the same line of code has different security properties depending on the interpreter running it — and it broke legitimate uses that relied on loading richer types, prompting some teams to "fix" the breakage by switching to unsafe_load.
If you see unsafe_load introduced during a Ruby upgrade, treat it as a finding: the correct migration is usually safe_load with an explicit permitted_classes list.
It is safe against object instantiation with a conservative permitted-classes list. Be careful what you add to that list — permitting a class with side effects in its initialisation reopens the door. Also set aliases: false unless you need them, since alias expansion enables a denial-of-service variant.
A YAML file you control and ship with the application is trusted input, so unsafe_load is defensible there. The distinction is provenance, not file extension — a config file uploaded by a user is untrusted.
Only for data that never leaves your trust boundary. It has no safe mode and no allowlist mechanism, so any attacker-influenced Marshal payload is a compromise.