Ruby Deserialization Audit
TL;DR: Auditors look for
Marshal.load,YAML.load/YAML.unsafe_load, ERB on user input, and any Rails app whosesecret_key_basehas leaked — because each gives the attacker an arbitrary Ruby object graph, and Rails ships gadget chains that turn that into RCE.
What it is
Ruby’s Marshal and Psych (YAML) deserialisers will instantiate arbitrary classes and call methods like init_with or marshal_load during reconstruction. Rails extends the blast radius: signed cookies and ActiveSupport::MessageVerifier payloads are Marshal-encoded by default, so a leaked secret_key_base is equivalent to RCE. ERB templates rendered from user input are another classic SSTI sink that auditors must trace from controllers and mailers.
Preconditions / where it applies
- Ruby 3.x, Rails 5–7, Sinatra, Sidekiq, dRuby
- Sinks live in session/cookie stores, cache layers (
Rails.cache.fetch), background job arguments, and admin reporting features - Safe-looking patterns:
YAML.load(File.read(path)),Marshal.load(redis.get(key)),ERB.new(params[:tmpl]).result(binding)
Technique
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. Marshal.load on attacker-controlled bytes — Universal RCE gadget
data = Base64.decode64(params[:state])
state = Marshal.load(data) # any object with marshal_load can run code
# 2. YAML.load (pre-Psych-4) instantiates arbitrary classes
require 'yaml'
YAML.load(request.body.read) # payload uses !ruby/object:Gem::Installer
# 3. Rails cookie forgery after secret_key_base leak
# Attacker re-signs a Marshal payload with the leaked key:
verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal)
cookie = verifier.generate(evil_object) # paste into session cookie
# 4. ERB SSTI in a reporting endpoint
require 'erb'
ERB.new(params[:report]).result(binding)
# payload: <%= `id` %>
Detection and defence
- Semgrep:
ruby.lang.security.dangerous-yaml-load,ruby.lang.security.marshal-load,ruby.rails.security.dangerous-eval - Brakeman warnings:
UnsafeDeserialization,UnsafeReflection,Evaluation - Replace with
YAML.safe_load(input, permitted_classes: [Date]),JSON.parse, and signed JWTs overMarshalpayloads - Rotate
secret_key_baseviabin/rails credentials:editif leak suspected, force-expire sessions, and configureRails.application.config.action_dispatch.cookies_serializer = :json - Never call
ERB.newon request data — pre-render fixed templates with locals
References
- Rails security guide — cookie + secret_key_base section
- Psych 4 release notes —
loadvsunsafe_loadchange - Brakeman documentation — warning catalogue
See also: source-sink-flow-analysis, dangerous-java-sinks, python-dangerous-sinks.