YAML.safe_load and Safe Serialization

· 5 min read · Updated March 19, 2026 · intermediate
ruby yaml serialization security stdlib

YAML is Ruby’s go-to format for configuration files, data exchange, and serialization. Rails uses it, Bundler uses it, Docker Compose uses it. The standard library YAML module handles parsing and generating YAML. But loading YAML from the wrong source can hand an attacker complete control of your server. This guide covers how safe_load works, what it protects against, and how to use it correctly.

Why YAML.load Is Dangerous

YAML has tags that represent Ruby objects. The !ruby/object tag tells the parser to instantiate any Ruby class:

malicious_yaml = <<~YAML
  --- !ruby/object:OpenWithShellExpansion
    cmd: "echo pwned > /tmp/pwned.txt"
YAML

YAML.load(malicious_yaml)
# Runs the shell command. You've been owned.

YAML.load can instantiate any Ruby object, not just “safe” data types. This is a well-known deserialization vulnerability class. Real-world exploits have compromised servers through YAML loading. If an attacker can feed a YAML string into your application, they can execute arbitrary code.

YAML.safe_load: The Safe Alternative

YAML.safe_load restricts which classes can be deserialized. It raises Psych::DisallowedClass for any tag not on an explicit whitelist.

YAML.safe_load(malicious_yaml)
# => Psych::DisallowedClass: Tried to load unspecified class: OpenWithShellExpansion

The attacker’s payload is neutralized — the program crashes safely instead of running their code.

Basic types are always allowed regardless of permitted_classes: String, Integer, Float, TrueClass, FalseClass, NilClass, Array, Hash.

The safe_load API

YAML.safe_load(yaml, permitted_classes: [], permitted_symbols: [], aliases: false, date: nil, permitted_regexps: [])

All parameters are keyword arguments.

Parameters:

ParameterTypeDefaultDescription
permitted_classesArray of Class/String[] (Ruby 3.1+), [Symbol, Date] (Ruby 3.0)Extra classes to allow
permitted_symbolsArray of Symbol[]Bare symbols to allow
aliasesBooleanfalse (Ruby 3.1+), true (Ruby 3.0)Whether to resolve &anchor / *alias
dateClass or nilnil (Ruby 3.1+), Date (Ruby 3.0)Class to parse bare dates as
permitted_regexpsArray of Regexp[] (Ruby 3.2+)Allowed !ruby/regexp tags

permitted_classes: Whitelisting Safe Classes

You often need to load custom classes you serialized yourself. Use permitted_classes to explicitly allow them:

yaml_with_dates = <<~YAML
  created: 2023-01-15
  updated: 2023-06-20
YAML

# Without permission, dates come back as strings
YAML.safe_load(yaml_with_dates)
# => {"created"=>"2023-01-15", "updated"=>"2023-06-20"}

# With Date permitted, they become Date objects
YAML.safe_load(yaml_with_dates, permitted_classes: [Date])
# => {"created"=>#<Date: 2023-01-15>, "updated"=>#<Date: 2023-06-20>}

Common values for permitted_classes:

ClassUse case
DateConfig files with date values
TimeTimestamps
SymbolHash keys serialized as symbols
RegexpRuby 3.2+: !ruby/regexp tag

Never add classes you don’t need. Each permitted class is a potential attack surface.

Ruby 3.1: The Breaking Change

Ruby 3.1 changed all safe_load defaults to be more restrictive. This broke many applications on upgrade.

What changed:

  • permitted_classes default changed from [Symbol, Date] to [] — no custom classes allowed by default
  • aliases default changed from true to false — YAML aliases must be explicitly enabled
  • date default changed from Date to nil — bare dates return strings, not Date objects
# Ruby 3.0 — Date allowed automatically
YAML.safe_load("--- 2023-01-15")
# => #<Date: 2023-01-15>

# Ruby 3.1+ — Date requires explicit permission
YAML.safe_load("--- 2023-01-15", permitted_classes: [Date])
# => #<Date: 2023-01-15>

If you’re upgrading to Ruby 3.1+ and seeing Psych::DisallowedClass errors on Date objects, add permitted_classes: [Date] to your calls.

Aliases and DoS Protection

YAML aliases let you reference the same object without duplicating it:

yaml_alias = <<~YAML
  defaults: &defaults
    timeout: 30
    retries: 3
  production:
    <<: *defaults
    timeout: 60
YAML

Aliases look useful, but they enable DoS attacks. Attackers can craft YAML with exponential alias expansion, causing the parser to consume huge amounts of CPU and memory:

# Malicious YAML that creates exponential parsing time
# (simplified example — real exploits are more sophisticated)
evil = "--- &a [" * 1000 + "]" * 1000
YAML.safe_load(evil, aliases: true)  # Could hang your server

Ruby 3.1+ disables aliases by default. If you need them, explicitly enable them only for trusted YAML:

YAML.safe_load(trusted_yaml, aliases: true)

For any untrusted source, keep aliases disabled.

Common Errors

Psych::DisallowedClass

Raised when the YAML contains a tag for a class not in permitted_classes:

YAML.safe_load("--- !ruby/object:OpenWithShellExpansion\n  cmd: id")
# => Psych::DisallowedClass: Tried to load unspecified class: OpenWithShellExpansion

Fix: either remove the untrusted YAML source, or add the class to permitted_classes if you trust it.

Symbol Handling in Ruby 3.1+

Ruby 3.1+ doesn’t allow bare symbols by default. If your YAML has symbol keys and you’re upgrading from Ruby 3.0:

# Symbol keys as hash values work fine (strings are used)
YAML.safe_load("{ foo: 1, bar: 2 }")
# => {"foo"=>1, "bar"=>2}

# But !ruby/symbol tag needs explicit permission
YAML.safe_load("--- !ruby/symbol\nfoo", permitted_symbols: [:foo])
# => {:foo}

Practical Patterns

Loading Application Config

require 'yaml'

def load_config(path)
  yaml_content = File.read(path)
  YAML.safe_load(yaml_content, permitted_classes: [Date, Symbol])
end

config = load_config('/etc/myapp/config.yml')

Handling Untrusted User YAML

Always use safe_load for user-submitted YAML. Catch and handle the error:

def parse_user_yaml(raw_yaml)
  YAML.safe_load(raw_yaml, permitted_classes: [Date])
rescue Psych::DisallowedClass => e
  Rails.logger.warn "Blocked disallowed YAML class: #{e.class_name}"
  nil
end

Inspecting YAML Before Loading

Debug what tags are in a YAML string without executing it:

require 'yaml'

def yaml_tags(yaml_string)
  YAML.parse(yaml_string).children.flat_map do |node|
    tag = node.tag&.sub('tag:yaml.org,2002:', '')
    [tag] + (node.children.map { |c| c.tag&.sub('tag:yaml.org,2002:', '') }.compact if node.children?)
  end.flatten.compact.uniq
end

yaml_tags("--- !ruby/object:Time\nnow: 2023")
# => ["ruby/object", "ruby/object:Time"]

If ruby/object appears in the output and you didn’t expect it — don’t load that YAML.

MethodSafe?Notes
YAML.load(yaml)NoCan instantiate any Ruby object. Never use with untrusted input.
YAML.unsafe_load(yaml)NoRuby 3.1+ alias for YAML.load.
YAML.dump(object)YesSerializes a Ruby object to YAML string.
YAML.safe_load(yaml)YesUse for everything.
Psych.safe_loadYesEquivalent to YAML.safe_load.

See Also