Rails ships with strong security defaults, but most serious Rails vulnerabilities come from code and configuration that quietly step around them. String-built SQL fragments, html_safe on user content, a leaked master.key, a permit! call or a missing config.hosts entry each undo a protection the framework already provides. This guide covers what Rails protects by default, the configuration settings that matter in production, safe and unsafe coding patterns, credentials and key management, and the dependency and scanning workflow. It is written for Rails developers, application security engineers and reviewers who need specific settings and method names rather than general advice.
Key takeaways
- Know which defaults you inherit. Encrypted cookie sessions, CSRF tokens, output escaping, parameterised Active Record finders and four default security headers are on in a new app;
config.load_defaultsdecides several of them, so an old value there means old behaviour. - Most Rails bugs are bypasses. Interpolated strings in
where,rawandhtml_safe,redirect_to params[:url],render file:with user input andpermit!are the recurring review findings. - The master key is the crown jewel.
config/master.keyorRAILS_MASTER_KEYdecrypts every secret incredentials.yml.enc, andsecret_key_basesigns and encrypts every session cookie. Treat both as production secrets with an owner and a rotation plan. - Production configuration is a security control.
force_ssl,config.hosts,filter_parameters,raise_on_open_redirectsandconsider_all_requests_local = falsebelong in a reviewed baseline, not in individual developer memory. - Use the scanners Rails now generates. New apps include Brakeman and bundler-audit in the Gemfile and the generated CI workflow; keep them, gate merges on them and stay on a supported Rails series.
What Rails protects by default
Rails security is a set of layered defaults across Action Pack, Action View and Active Record. The table shows what each default does, where it is controlled and how it is typically broken. Defaults tied to config.load_defaults only apply when the app's target version is new enough, which is why upgraded applications often lag behind freshly generated ones.
| Protection | Default behaviour | Setting or API | How it is usually bypassed |
|---|---|---|---|
| Session integrity | ActionDispatch::Session::CookieStore, cookie encrypted and signed with keys derived from secret_key_base | config.session_store, credentials | Leaked secret_key_base, secrets committed to Git |
| CSRF | Forgery protection on ActionController::Base (default since 5.2 load defaults) | protect_from_forgery, csrf_meta_tags | skip_forgery_protection on state-changing actions, GET routes that change state |
| Output escaping | ERB <%= %> escapes HTML | Action View helpers, sanitize | raw, html_safe, <%== %>, unsafe link_to hrefs |
| SQL parameterisation | Hash conditions and finders such as find and find_by(email: x) quote values | Active Record query interface | String interpolation in where, order, pluck, find_by_sql |
| Mass assignment | Strong parameters reject unpermitted keys; permit_all_parameters is false | params.expect, params.require(...).permit(...) | permit!, permitting role or admin keys |
| Security headers | X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, X-Permitted-Cross-Domain-Policies: none, Referrer-Policy: strict-origin-when-cross-origin | config.action_dispatch.default_headers | default_headers.clear, removing headers to allow framing |
| Host header | ActionDispatch::HostAuthorization active in development only | config.hosts, config.host_authorization | Never configured in production, so the protection is absent |
| Log filtering | Generated initialiser filters :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc | config.filter_parameters | Custom parameter names (for example api_credential) that no pattern matches |
See the OWASP Top 10 Guide for the vulnerability classes these defaults address, and the Security Headers Guide for header values beyond the Rails defaults.
Production configuration baseline
Most of the Rails attack surface that is not in application code lives in config/environments/production.rb and config/application.rb. Review these settings in every release and treat a change to them as a security change.
| Setting | Recommended production value | Why it matters |
|---|---|---|
config.force_ssl | true | Redirects to HTTPS, marks cookies secure and adds Strict-Transport-Security via ActionDispatch::SSL; config.ssl_options includes subdomains in HSTS from the 5.0 defaults onwards |
config.assume_ssl | true only behind a TLS-terminating proxy | Stops redirect loops and HTTP cookie targeting when the proxy forwards plain HTTP |
config.hosts | Explicit list of production host names | Enables HostAuthorization against DNS rebinding and Host header poisoning of generated URLs and password reset links; exclude health checks with config.host_authorization = { exclude: ... } |
config.consider_all_requests_local | false | true returns full exceptions and exposes /rails/info/properties |
config.require_master_key | true | App refuses to boot without a key, rather than starting with missing credentials |
config.action_controller.raise_on_open_redirects | true | Raises UnsafeRedirectError for external hosts unless the call passes allow_other_host: true |
config.action_controller.forgery_protection_origin_check | true | Adds an Origin header check to CSRF token verification |
config.action_controller.default_protect_from_forgery_with | :exception | The historical default is :null_session; the Rails configuration guide records :exception as the new default from the 8.2 load defaults, and older apps can opt in early |
config.action_controller.action_on_unpermitted_parameters | :raise in test, :log in development | Turns silent parameter drops into failing tests, which catches mass assignment mistakes early |
config.action_dispatch.cookies_same_site_protection | :lax or :strict | Adds a browser-side CSRF defence for cookies |
Run bin/rails app:update on each upgrade and work through new_framework_defaults_*.rb so that the app's load_defaults value moves forward with the framework. A Rails 8 app still running with load_defaults 5.1 silently lacks several protections in the table above.
Safe and unsafe coding patterns
The Rails security guide and the OWASP Ruby on Rails Cheat Sheet agree on a short list of methods that deserve a mandatory second look in code review. Each row below pairs the risky pattern with its safe replacement.
| Risk | Unsafe pattern | Safe pattern |
|---|---|---|
| SQL injection | Project.where("name = '#{params[:name]}'"), interpolated order or find_by_sql | where(name: params[:name]), positional where("zip = ?", zip) or named placeholders, sanitize_sql, an allow-list for sort columns |
| XSS | raw(user_input), user_input.html_safe, link_to "x", params[:url] | Default ERB escaping, sanitize(input, tags: [...], attributes: %w(href title)) with a permitted list, URL scheme validation |
| Mass assignment | User.new(params[:user]) with permit! | params.expect(user: [:name, :email]), never permit privilege fields from user forms |
| Command injection | system("convert #{file}"), backticks, exec, spawn with a single string | system("convert", file) (argument form), allow-listed values |
| Kernel#open | open(params[:path]) (a leading pipe runs a command) | File.open, URI.open on a validated URI |
| Path traversal | send_file("/var/www/uploads/" + params[:filename]), render file: params[:template] | Look up files by record ID, check the expanded path stays under the intended directory, never render user-chosen templates |
| Open redirect | redirect_to params[:return_to] | raise_on_open_redirects, url_from(params[:return_to]) or a path-only allow-list |
| Regex validation | /^https?:\/\/[^\n]+$/ | \A and \z anchors; Rails format validators raise if ^ or $ is used without multiline: true |
| Code execution | eval, constantize or send on user input | Explicit mapping hashes from input to allowed classes or methods |
Path traversal is not theoretical in Rails: CISA's Known Exploited Vulnerabilities catalogue lists three Rails file disclosure flaws, including CVE-2019-5418, where crafted Accept headers combined with render file: disclosed arbitrary files. See the SQL Injection Prevention Guide, the Cross-Site Scripting (XSS) Prevention Guide and the Input Validation and Output Encoding Guide for the general techniques.
Authorisation is your code, not the framework's
Rails has no built-in object-level authorisation. Project.find(params[:id]) returns any project to any signed-in user. Scope lookups through the owner (current_user.projects.find(params[:id])) or a policy layer, and test that one tenant cannot read another tenant's records. This is the insecure direct object reference that the OWASP cheat sheet lists, and the one Brakeman cannot fully detect because it depends on business rules.
Sessions, authentication and CSRF
- Cookie sessions: the default
CookieStorekeeps the session in the client, encrypted. The Rails guide notes a 4 kB size limit and warns that clients may keep and replay old cookies, so store identifiers rather than sensitive data and do not keep values such as account balances in the session. - Session fixation: call
reset_sessionafter a successful sign-in so a pre-login session identifier cannot be reused. The authentication generator in recent Rails and gems such as Devise handle this on sign-in and sign-out; hand-rolled login code often does not. See the Session Fixation Guide. - Server-side expiry: because a client can edit cookie expiry, expire sessions on the server. With the database-backed sessions created by the authentication generator, delete records older than your idle and absolute limits on a schedule.
- Passwords:
has_secure_passwordstores bcrypt hashes andauthenticate_bylooks up and verifies credentials in one call, which avoids timing differences between unknown and wrong-password accounts. - Brute force: Rails 8 includes a controller rate limiter.
rate_limit to: 10, within: 3.minutes, only: :createon the sessions controller is the example in the Rails security guide; pair it with generic error messages. - CSRF: keep
protect_from_forgery with: :exceptionandcsrf_meta_tagsin the layout, use non-GET verbs for every state change, and never addskip_forgery_protectionto a cookie-authenticated controller. API controllers authenticated by bearer tokens inherit fromActionController::API, which does not include forgery protection, and must not also accept the session cookie. See the CSRF Prevention Guide.
Credentials, secret_key_base and encryption
Rails stores application secrets in config/credentials.yml.enc, edited with bin/rails credentials:edit and decrypted with config/master.key or the RAILS_MASTER_KEY environment variable. The encrypted file may be committed; the key must never be. Per-environment files such as config/credentials/production.yml.enc with their own keys let you restrict who can read production secrets.
| Secret | What it protects | Exposure impact | Rotation approach |
|---|---|---|---|
master.key / RAILS_MASTER_KEY | Every value in the credentials file | All API keys, database passwords and secret_key_base readable | Re-encrypt credentials under a new key, then rotate every secret the file held |
secret_key_base | Signed and encrypted cookies, sessions, message verifiers, signed IDs | Forged sessions and cookies for any user | Add a cookie rotator reading the old value (for example old_secret_key_base), deploy the new value, remove the old one once cookies have expired |
| Active Record encryption keys | Attributes declared with encrypts | Decryption of protected columns | Generate with bin/rails db:encryption:init; keep previous keys configured until data is re-encrypted |
| Third-party API keys | Payment, email, cloud and AI services | Direct abuse of the provider account | Rotate in the provider, update credentials, monitor provider audit logs |
- Fail loudly: use
Rails.application.credentials.some_api_key!so a missing secret raises aKeyErrorinstead of sending blank values to a provider. - Protect columns, not just transport: Active Record encryption is non-deterministic by default; use
deterministic: trueonly for columns you must query by exact match, such as an email address, because deterministic ciphertexts reveal equality. - Keep secrets out of build output: container images built with
RAILS_MASTER_KEYas a build argument keep it in image history. Supply the key at runtime from a secrets manager. See the Secrets Management Guide.
Where identity fits
A production Rails app is itself a non-human identity: it holds a database role, cloud credentials for Active Storage, SMTP and payment API keys, and often a RubyGems or GitHub token in its CI pipeline. Each of these should be scoped to the app, owned by a named team, rotated on a schedule and revocable without a redeploy. Background jobs (Solid Queue, Sidekiq) usually run with the same credentials as the web process; consider separate, narrower credentials for job workers that call external services. See the OWASP NHI Top 10 NHI7 Long-Lived Secrets Guide.
Headers, CSP and browser controls
Rails provides a Content Security Policy DSL in config/initializers/content_security_policy.rb, with per-request nonces through content_security_policy_nonce_generator and a report-only mode (content_security_policy_report_only = true) for testing. Start in report-only mode with a report_uri, fix violations, then enforce. A Permissions Policy DSL lives in config/initializers/permissions_policy.rb. Rails has no built-in CORS middleware; the Rails guide uses the rack-cors gem in config/initializers/cors.rb, and origins there should be an explicit list, never * with credentials. See the Content Security Policy Guide and the Clickjacking Guide.
Dependencies, scanning and upgrades
- Static analysis: new Rails apps include
brakemanin the Gemfile and a generated CI job that runs it. Brakeman understands Rails data flow and finds SQL injection, XSS, unsafe redirects, mass assignment and dangerousrendercalls. Review its ignore file (config/brakeman.ignore) like code. - Gem advisories: new apps also include
bundler-audit, which checksGemfile.lockagainst the Ruby Advisory Database; ignored advisories go inconfig/bundler-audit.ymland should carry an expiry and an owner. - Conservative updates: the Rails guide recommends
bundle update --conservative gem_nameso that patching one vulnerable gem does not pull unrelated upgrades. - Supported series: the Rails maintenance policy gives each minor series bug fixes for one year and security fixes for two years from its first release. When this guide was written, 8.1 received security fixes until 10 October 2027 and 8.0 until 7 November 2026; older series get no new releases.
- Registry trust: the GemStuffer RubyGems campaign pushed thousands of malicious gems and tried to harvest maintainers' API keys. Pin versions in
Gemfile.lock, review new gems before adding them, and enable MFA on RubyGems accounts. See the Malicious Package and Dependency Confusion Guide.
How to implement: a phased rollout
- Baseline (week 1): record each app's Rails version,
load_defaultsvalue and production configuration against the baseline table. Flag any app on an unsupported series. - Scanners in report mode (weeks 1 to 2): add Brakeman and bundler-audit to CI without failing builds, triage the backlog and record accepted findings with an owner.
- Secrets (weeks 2 to 4): confirm
master.keyis in.gitignore, search Git history for leaked keys, moveRAILS_MASTER_KEYinto a secrets manager and rotate anything that was ever committed. - Configuration hardening (weeks 3 to 6): set
config.hosts,force_ssl,raise_on_open_redirectsand origin checks in staging first; run the full test suite and smoke tests; roll back by reverting the config commit if health checks or redirects fail. - CSP (weeks 4 to 8): deploy a report-only policy, collect violations for at least one release cycle, then enforce. Rolling back is a one-line switch back to report-only.
- Gate and maintain (ongoing): fail merges on new high-confidence Brakeman warnings and unpatched advisories, and schedule framework upgrades before the series leaves security support.
Small teams can stop after steps 1 to 4 plus the scanners and still remove most of the common risk. Larger organisations with many Rails services should publish the baseline as a shared template or generator and measure drift against it.
Detection and measurement
- Log signals:
ActionController::InvalidAuthenticityTokenspikes (CSRF probing or a broken form),ActionDispatch::HostAuthorizationblocked hosts,UnsafeRedirectError, rate limiter rejections on sessions and password reset, andActiveRecord::StatementInvaliderrors with quote characters in parameters. - Log hygiene: test that
filter_parametersmasks every secret-bearing field by grepping production logs for known parameter names. - Metrics: percentage of apps on a supported Rails series; open Brakeman high-confidence warnings per app; median days from a Ruby advisory to patched production; number of apps with
load_defaultsmore than one major version behind; apps with an enforced CSP; days since lastsecret_key_baseor master key rotation.
Framework mapping
| Rails control area | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | OWASP ASVS 5.0 | MITRE ATT&CK addressed |
|---|---|---|---|---|
| Parameterised queries, escaping, strong parameters | SI-10 | PR.PS-06 | V1.2.4, V15.3.3 | T1190 Exploit Public-Facing Application |
| Sessions and cookie keys | SC-23 | PR.AA-05 | V3.3.1 | T1539 Steal Web Session Cookie |
| TLS, HSTS, headers | SC-8 | PR.DS-02 | V3.4.1, V3.4.4 | |
| Credentials and encryption keys | SC-12, SC-28 | PR.DS-01 | V13.3.1 | T1552.001 Credentials In Files |
| Brakeman, bundler-audit, upgrades | SA-11, RA-5, SR-3 | ID.RA-01 | V15.2.1 | T1195.002 Compromise Software Supply Chain |
| Production error pages | SA-15 | PR.PS-06 | V13.4.2 |
In CIS Controls v8.1 this work sits in Control 16 (Application Software Security), and in ISO/IEC 27001:2022 Annex A in 8.25 (secure development life cycle), 8.28 (secure coding) and 8.24 (use of cryptography).
Common failure patterns
- Committed master key:
config/master.keyis added to a repository or baked into a container image, exposing every credential at once. Exposed.gitfolders and public images are both common routes for such leaks. - Stale load_defaults: an app upgraded across several majors without updating
load_defaultsruns with:null_sessionCSRF handling, no origin check and older cookie settings. - Interpolated order and pluck: developers who parameterise
wherestill passparams[:sort]intoorder, which takes raw SQL. - html_safe on helper output: a helper that concatenates user data and calls
html_safeturns every view that uses it into an XSS sink. - Unscoped finders:
Model.find(params[:id])in multi-tenant apps exposes other customers' records; scanners rarely catch it. - render file: with user input: the CVE-2019-5418 pattern; even on patched Rails, choosing a template from a parameter remains a path traversal risk.
Practitioner checklist
- Confirm every app runs a Rails series inside its security support window and that
load_defaultsmatches the framework version. - Set
force_ssl,config.hosts,require_master_key,raise_on_open_redirectsandconsider_all_requests_local = falsein production. - Keep
master.keyout of Git and images; supplyRAILS_MASTER_KEYat runtime and document how to rotate it andsecret_key_base. - Ban
permit!, string-interpolated SQL,raw/html_safeon user data andrender file:with parameters through code review rules and Brakeman. - Call
reset_sessionon sign-in, expire sessions server-side and rate limit login and password reset. - Scope every record lookup to the current user or tenant and test cross-tenant access.
- Run Brakeman and bundler-audit in CI with reviewed ignore files that carry owners and expiry dates.
- Extend
filter_parametersfor custom secret field names and verify logs are clean. - Deploy a nonce-based CSP in report-only mode, then enforce it.
Standards and references
- Ruby on Rails Guides: Securing Rails Applications
- Ruby on Rails Guides: Configuring Rails Applications
- Ruby on Rails Guides: Active Record Encryption
- Ruby on Rails: Maintenance Policy
- OWASP: Ruby on Rails Cheat Sheet
- OWASP: Application Security Verification Standard 5.0
- CISA: Known Exploited Vulnerabilities Catalog
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: Django Security Guide · Secure Code Review Guide · Software Supply Chain Security Guide · Secrets Management Guide