{"id":18520907,"url":"https://github.com/stevenjcumming/rails-security-guide","last_synced_at":"2026-04-15T22:36:00.659Z","repository":{"id":177831283,"uuid":"660983498","full_name":"stevenjcumming/rails-security-guide","owner":"stevenjcumming","description":"Ruby on Rails guide, checklist, and tips for security audits and best practices","archived":false,"fork":false,"pushed_at":"2023-07-01T12:24:44.000Z","size":8,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-23T02:58:27.823Z","etag":null,"topics":["checklist","rails","rails-security","ruby","security"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stevenjcumming.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-01T12:21:20.000Z","updated_at":"2023-07-01T14:06:40.000Z","dependencies_parsed_at":null,"dependency_job_id":"e2af3e95-5e8c-4e81-a036-45c6d7afcbf3","html_url":"https://github.com/stevenjcumming/rails-security-guide","commit_stats":null,"previous_names":["stevenjcumming/rails-security-guide"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/stevenjcumming/rails-security-guide","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevenjcumming%2Frails-security-guide","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevenjcumming%2Frails-security-guide/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevenjcumming%2Frails-security-guide/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevenjcumming%2Frails-security-guide/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stevenjcumming","download_url":"https://codeload.github.com/stevenjcumming/rails-security-guide/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevenjcumming%2Frails-security-guide/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31863495,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-15T15:24:51.572Z","status":"ssl_error","status_checked_at":"2026-04-15T15:24:39.138Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["checklist","rails","rails-security","ruby","security"],"created_at":"2024-11-06T17:23:26.757Z","updated_at":"2026-04-15T22:36:00.623Z","avatar_url":"https://github.com/stevenjcumming.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Rails Security Guide\n\n## Overview\nDisclaimer: This security guide isn't intended to be exhaustive \n\nUse this guide before each deployment, or, even better, use an automated process.\n\n**Definitions**\n- _Never_: Never means never\n- _Don't_: Don't unless you have a really really good reason\n- _Avoid_: Avoid unless you have a good reason\n\n**Gems**\n* [Brakeman](https://github.com/presidentbeef/brakeman) - A static analysis security vulnerability scanner for Ruby on Rails applications\n* [Rack::Attack!!](https://github.com/kickstarter/rack-attack) - Rack middleware for blocking \u0026 throttling\n* [SecureHeaders](https://github.com/github/secure_headers) - Security related headers all in one gem\n* [Sanitize](https://github.com/rgrove/sanitize) - An allowlist-based HTML and CSS sanitizer\n* [zxcvbn](https://github.com/bitzesty/devise_zxcvbn) - Devise plugin to reject weak passwords using zxcvbn\n* [StrongPassword](https://github.com/bdmac/strong_password) - Entropy-based password strength checking for Ruby and Rails\n* [Pundit](https://github.com/varvet/pundit) - Minimal authorization through OO design and pure Ruby classes\n\n**TOC**\n* [Injections](#injections)\n* [Cross-site Scripting (XSS)](#cross-site-scripting)\n* [Authentication and Sessions](#authentication-and-sessions)\n* [Authorization](#authorization)\n* [Cross-Site Request Forgery](#cross-site-request-forgery)\n* [Insecure Direct Object Reference or Forceful Browsing](#insecure-direct-object-reference-or-forceful-browsing)\n* [Redirects](#redirects)\n* [Files](#files)\n* [Cross-Origin Resource Sharing](#cross-origin-resource-sharing)\n* [Data Leaking and Logging](#data-leaking-and-logging)\n* [Misc](#misc)\n\n## Injections\n- [ ] Parameterize or serialize user input (including URL query params) before using it\n- [ ] Don't pass strings as parameters to Active Records methods. Use arrays or hashes instead\n- [ ] Never use user input directly when using the `delete_all` method\n- [ ] Never use user input in system commands\n- [ ] Avoid system commands \n- [ ] Sanitize ALL hand-written SQL [ActiveRecord Sanitization](https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html)\n\n```\n# bad \nUser.find_by(\"id = '#{params[:user_id]'\")\n\nUser.delete_all(\"id = #{params[:user_id]}\")\n\nUser.where(admin: false).group(params[:group])\nUser.where(\"name = '#{params[:name]'\")\n\n# good\nUser.find(id)\nUser.find_by(id: params[:id])\nUser.find_by_id(params[:id].to_i) # better\n\nUser.where({ name: params[:name] })\nUser.where(admin: false).group(:name)\nUser.where(\"name LIKE ?\", \"#{params[:search]}%\")\nUser.where(\"name LIKE ?\", User.sanitize_sql_like(params[:search]) + \"%\")\n```\n\n## Cross-site Scripting\nBy default, when string data is shown in views, it is escaped prior to being sent back to the browser.\n\n- [ ] Never disable `ActiveSupport#escape_html_entities_in_json`\n- [ ] Don't use `raw`, `html_safe`, `content_tag`, or `\u003c%==`\n- [ ] Prefer Markdown over HTML\n- [ ] Validate and sanitize user input for Urls and Html (including classes or attributes)\n- [ ] Never create templates in code (use ERB, Slim, Haml, etc)\n- [ ] Never use `render inline` or `render text`\n- [ ] Never use unquoted variables in HTML attribute\n- [ ] Don't use template variables in script blocks \n- [ ] Implement [Content Security Policy](https://guides.rubyonrails.org/v7.0/security.html#content-security-policy-header\n) or use SecureHeaders gem if below Rails v5.2 \n\n```\n# bad \nconfig.action_view.escape_html_entities_in_json = false\n\u003c%= raw @user.bio %\u003e\n\u003c%= @user.bio.html_safe %\u003e\n\u003c%= link_to \"Personal Website\", @user.personal_website %\u003e\n\n\u003cdiv class=\u003c%= params[:css_class] %\u003e\u003c/div\u003e\n\u003cscript\u003evar name = \u003c%= @user.name %\u003e;\u003c/script\u003e\nrender inline: \"\u003cdiv\u003e#{@user.name}\u003c/div\u003e\"\n\n# good\nsanitize(@user.bio, tags: %w(b br em i p strong), attributes: %w())\nstrip_tags(\"Strip \u003ci\u003ethese\u003c/i\u003e tags!\") # =\u003e Strip these tags!\nstrip_links('\u003ca href=\"http://www.rubyonrails.org\"\u003eRuby on Rails\u003c/a\u003e') # =\u003e Ruby on Rails\n\nvalidates :instagram, url: true, allow_blank: true # link_to(\"Instagram\", @user.instagram)\nvalidates :color, hex_color: true # HexColorValidator # \u003cdiv style=\"background-color: \u003c% user.color %\u003e\"\u003e\n```\n\n## Authentication and Sessions\n- [ ] Use a database based session store\n- [ ] Never put sensitive information in the session\n- [ ] Set an expiration for the session (Limit: 30 minutes)\n- [ ] Limit \"Remember Me\" functionality to 2 weeks\n- [ ] The same timeline can be used for access \u0026 refresh tokens\n- [ ] Set all cookies and session store as httponly and secure\n- [ ] Revalidate cookie values\n- [ ] Never store \"state\" in the session or a cookie\n- [ ] Enforce password complexity (min length, no words, etc)\n- [ ] Consider captcha on publicly available forms \n- [ ] Consider captcha after several failed login attempts \n- [ ] Always confirm user emails \n- [ ] Require old password to change password (except for forgot password)\n- [ ] Expire password reset tokens after 10 minutes\n- [ ] Limit password reset emails within a specified timeframe \n- [ ] Consider using two-factor authentication (2FA) (required if storing sensitive data)\n- [ ] Don't use \"Security Questions\"\n- [ ] Use generic error messages for failed login attempts (Email or password is invalid)\n- [ ] add `before_action :authenticate_user!` to ApplicationController and `skip_before_action :authenticate_user!` to publicly accessible controllers/actions.\n\n```\n# bad\nRails.application.config.session_store :my_custom_store, expire_after: 2.years\nJWT.encode payload, nil, 'none'\n\n# good\nRails.application.config.session_store :active_record_store, expire_after: 30.minutes, httponly: true, secure: true\ncookies[:login] = {value: \"user\", httponly: true, secure: true}\nJWT.encode({ data: 'data', exp: Time.now.to_i + 4 * 3600 }, hmac_secret, 'HS256')\nconfig.force_ssl = true\n```\n\n## Authorization\n- [ ] NEVER do authorization on the frontend\n- [ ] Admin interface should be isolated from the user interface\n- [ ] Use 2FA on the admin interface\n- [ ] Don't use `accepts_nested_attributes_for` for permissions \n- [ ] Prefer policies over querying by association (current_user.posts)\n- [ ] Always use policies if using multi-user accounts   \n\n```\n# bad\n@posts = Post.where(user_id: params[:user_id])\n@comment = Commend.find_by(id: params[:id])\naccepts_nested_attributes_for :permission\n\n# good\n@posts = current_user.posts\n@posts = policy_scope(Post)\n@comment = current_user.comments.find_by(id: params[:id])\nauthorize @post\n```\n\n## Cross-Site Request Forgery\n- [ ] If you use cookie-based authentication anywhere, use `protect_from_forgery`\n- [ ] If you use token-based authentication, you don't need `protect_from_forgery`\n\n```\n# Newer versions of Rails use:\nconfig.action_controller.default_protect_from_forgery\n\n# Implementation \nclass ApplicationController \u003c ActionController::Base\n  protect_from_forgery with: :exception\n\n  rescue_from ActionController::InvalidAuthenticityToken do |exception|\n    sign_out_user # destroy the user cookies\n  end\n  \n  ...(rest of file)...\nend\n```\n\n## Insecure Direct Object Reference or Forceful Browsing\nThis is basically guessing ids in the path: `https://example.com/user/10`\n\n- [ ] Use UUIDs, [hashids](https://github.com/peterhellberg/hashids.rb), or a non-guessable id\n- [ ] Avoid changing the default primary key (`id`)\n- [ ] Policies can help mitigate this as well\n- [ ] Don't let a user-supplied params to determine which view to render\n- [ ] Don't show the numerical `id` in an API call when using a uuid, hashid, etc\n\n```\nStripe Customer ID = cus_9s6XFG2Qq6Fe7v\n\n# don't do this\ndef show\n  render params[:user_supplied_view]\nend\n```\n\n## Redirects\n- [ ] Avoid passing any user-supplied params into `redirect_to`\n- [ ] If you must use user-supplied URLs for redirect_to... sanitize or use an allowlist\n- [ ] Validate with regex using \\A and \\z as anchors, _not_ ^ and $\n- [ ] If your needs are complex, use [Shopify's redirect_safely gem](https://github.com/shopify/redirect_safely)\n\n```\n# bad\nredirect_to params[:url]\nredirect_to URI.parse(params[:url]).path\nredirect_to URI.parse(\"#{params[:url]}\").host\nredirect_to \"https://yourwebsite.com/\" + params[:url]\n\n# ok, but not good\nredirect_to \"https://instagram.com/\" + params[:ig_username]\n\n# good\nredirect_to user.redirect_url # sanitize beforehand \nredirect_to AllowList.include?(params[:url]) ? params[:url] : '/'\n```\n\n## Files\n- [ ] Avoid user-generated filenames (e.g ../../passwd), assign random names if possible\n- [ ] Only allow alphanumeric, underscores, hyphens, and periods\n- [ ] Don't process images or videos on your server \n- [ ] Always (re)validate on the backend (file size, media type, name, etc.)\n- [ ] Process media files asynchronously\n- [ ] Use 3rd party scanners if necessary\n- [ ] Prefer cloud storage services such as Amazon S3 to directly handle file uploads and storage\n\n## Cross-Origin Resource Sharing \n- [ ] Use [rack-cors gem](https://github.com/cyu/rack-cors)\n- [ ] Unless your API is open to anyone, don't set wildcard as an origin. \n\n```\n# bad\nRails.application.config.middleware.insert_before 0, Rack::Cors do\n  allow do\n    origins'*'\n    resource '*', headers: :any, methods: :any\n  end\nend\n\n# good\nRails.application.config.middleware.insert_before 0, Rack::Cors do\n  allow do\n    origins'  http://example.com:80' # regular expressions can be used here\n    resource '*', headers: :any, methods: [:get, :post]\n  end\nend\n\nRails.application.config.middleware.insert_before 0, Rack::Cors do\n  allow do\n    origins' http://example.com:80'\n    resource '/orders',\n      :headers =\u003e :any,\n      :methods =\u003e [:post]\n    resource'/users',\n      headers: :any,\n      methods: [:get, :post, :put, :patch, :delete, :options, :head]\n  end\nend\n\nRails.application.config.middleware.insert_before 0, Rack::Cors do\n  allow do\n    if Rails.env.development?\n      origins 'localhost:3000', 'localhost:3001', 'https://yourwebsite.com'\n    else\n      origins' https://yourwebsite.com'\n    end\n\n    resource '*', \n      headers: :any, \n       methods: [:get, :post, :put, :patch, :delete, :options, :head]\n  end\nend\n```\n\n## Data Leaking and Logging\n- [ ] NEVER commit credentials, passwords, or keys \n- [ ] Use `config.filter_parameters` for sensitive data (passwords, tokens, etc)\n- [ ] Use `config.filter_redirect` for sensitive location you redirect to\n- [ ] Don't use 403 Forbidden for authorized errors (it implies the resource exists)\n- [ ] Don't include implementation details in view comments \n- [ ] Don't write your own encryption\n\n## Misc\n- [ ] [Encrypt](https://guides.rubyonrails.org/active_record_encryption.html) sensitive data at the application layer \n- [ ] Don't do this in routes `match ':controller(/:action(/:id(.:format)))\"`\n- [ ] Only use `https` gem sources\n- [ ] Use blocks for more than one gem source \n- [ ] Never set `config.consider_all_requests_local = true` in production\n- [ ] Separate gems by environment\n- [ ] Don't use development-related gems (better_errors) in public-facing environments\n- [ ] Don't make non-action controller methods public  \n- [ ] Use `JSON.parse` over `JSON.load`\n- [ ] Keep dependencies up-to-date and watch for vulnerabilities \n- [ ] Don't store credit card information\n- [ ] Avoid user-supplied data in emails to other users \n- [ ] Avoid user-created email templates (heavily sanitize or markdown if necessary)\n- [ ] Use `_html` for I18n keys with HTML tags \n\n\n**Additional Resources**\n* [Official Rails Security Guide](https://guides.rubyonrails.org/security.html)\n* [OWASP: Types of XSS](https://owasp.org/www-community/Types_of_Cross-Site_Scripting)\n* [OWAS: Session Management](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)\n* [Rails SQL Injections](https://rails-sqli.org/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevenjcumming%2Frails-security-guide","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstevenjcumming%2Frails-security-guide","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevenjcumming%2Frails-security-guide/lists"}