Ruby code auditing
TL;DR: Ruby audits in 2026 are 95% Rails. The bug families: mass assignment, unsafe deserialization (Marshal/YAML/JSON with class instantiation), Rails-specific
ActiveSupport::MessageVerifierkey leaks,renderwith user input, SQLi viafind_by_sql/wherestrings, command injection via backticks/Kernel.system. Cross-link ruby-deserialization-audit for the heavy chain detail.
What it is
Rails ergonomics are the audit’s both ally and enemy. Strong defaults (parameterised AR queries, CSRF baked in, escaping by default) eliminate whole bug classes. But the convenience methods (find_by_sql, where("name = '#{x}'"), render file: x) sit one keystroke away from RCE/SQLi/LFI. Non-Rails Ruby (Sinatra, Hanami, plain Rack) lacks the defaults entirely.
Preconditions / where it applies
- Source (Gemfile, app/, config/routes.rb)
- Rails major version — secret_key_base handling,
Marshalin cookies (<5.2),ActiveSupport::MessageVerifiershape - Knowledge of metaprogramming (
send,public_send,eval-family,instance_eval,class_eval,define_method)
Technique
- Map entry points.
- Rails:
config/routes.rb—resources,get/post/match, mounted engines. - Sinatra:
get '/path' do ... end. - Grape API:
class API < Grape::API; resource :foo; get :bar.
- Rails:
- Trace sources. Rails
params,cookies,session,request.headers. Strong parameters (params.require(:user).permit(:name)) is the mass-assignment guard — any controller usingparams[:user]directly orpermit!is suspect. - Sink catalogue.
1 2 3 4 5 6 7 8 9 10
rg -n '\beval\(|instance_eval\(|class_eval\(|module_eval\(|binding\.eval' . rg -n 'Marshal\.(load|restore)\(' . rg -n 'YAML\.(load|unsafe_load|load_stream)\(' . # safe: YAML.safe_load rg -n 'JSON\.load\(' . # JSON.load can instantiate; use JSON.parse rg -n 'send\(\s*params|public_send\(\s*params' . # method-name from input rg -n '`[^`]*#\{|system\(.*#\{|exec\(.*#\{|spawn\(.*#\{|IO\.popen\(.*#\{' . rg -n 'find_by_sql|where\([^)]*#\{|order\([^)]*#\{|group\([^)]*#\{' . # SQLi rg -n 'render\s*(?:file|inline|template):\s*params|redirect_to\s+params' . rg -n 'constantize|safe_constantize' . # arbitrary class lookup rg -n 'open\(\s*params|URI\.open|Net::HTTP\.get\(\s*params' . # SSRF / open() shell-pipe
- Mass assignment. Strong params is opt-in. Any controller calling
User.create(params)orupdate_attributes!(params[:user])without.permit(...)is mass-assignment. Look forpermit!(wildcard permit) — common in admin controllers, often shipped to users by accident. - Marshal.
Marshal.loadon any byte string under attacker control is RCE — gadget chains include AR, Rake, Gem::Requirement. Rails <5.2 stored session as Marshal-encoded cookie; ifsecret_key_baseleaks (heroku misconfig, env in repo), full RCE via cookie forgery. - YAML.
YAML.loadon attacker input instantiates Ruby objects → RCE via Psych gadgets (Gem::Specification,ERB,ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy). UseYAML.safe_load. Note: Psych 4 changed default —YAML.loadnow callssafe_loadon Ruby 3.1+. Audit version. sendabuse.obj.send(params[:method])= arbitrary method call including:eval,:system. Evenpublic_sendreaches private methods via__send__re-exposure in some patterns.- SQLi. AR is safe with hash conditions:
User.where(name: x). String conditions with interpolation are not:User.where("name = '#{x}'")orUser.find_by_sql("... #{x}").order(params[:sort])is SQLi if value used as raw SQL (no whitelist of column names). - Open redirect / SSRF.
redirect_to params[:next]→ open redirect.open(params[:url])in Ruby <2.7 invoked subprocess on URL starting with|— RCE. UseURI.parse+ scheme allowlist. - Render confusion.
render file: params[:f]reads any file (LFI).render inline: params[:t]is ERB → RCE.render template: ...looks up by name but..segments traverse. - constantize.
params[:type].constantizereturns the class — combined with.new(params)is mass-assign-anything + arbitrary-class-instantiation. Use a hash whitelist. - CSRF. Default ON for non-API;
protect_from_forgeryper-controller. APIs oftenskip_before_action :verify_authenticity_token— verify they use token auth not cookies. - Sessions and cookies. Check
config/initializers/session_store.rband Rails secrets:Rails.application.secrets.secret_key_baseexposure = full session forgery. Cookie serializer should be:json, not:marshal.
Detection and defence
- Brakeman — Rails-specific SAST, the de-facto audit tool. Run in CI; treat warnings as blockers.
bundle audit(orbundler-audit) for CVE deps.- Semgrep
ruby.rails.security. - For SQLi: forbid string-form
wherevia a Rubocop rule; require hash form. - Rotate
secret_key_baseon any suspicion of leak; old sessions/cookies become invalid (intended).