Skip to content

stevenjcumming/rails-security-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Rails Security Guide

Overview

Disclaimer: This security guide isn't intended to be exhaustive

Use this guide before each deployment, or, even better, use an automated process.

Definitions

  • Never: Never means never
  • Don't: Don't unless you have a really really good reason
  • Avoid: Avoid unless you have a good reason

Gems

  • Brakeman - A static analysis security vulnerability scanner for Ruby on Rails applications
  • Rack::Attack!! - Rack middleware for blocking & throttling
  • SecureHeaders - Security related headers all in one gem
  • Sanitize - An allowlist-based HTML and CSS sanitizer
  • zxcvbn - Devise plugin to reject weak passwords using zxcvbn
  • StrongPassword - Entropy-based password strength checking for Ruby and Rails
  • Pundit - Minimal authorization through OO design and pure Ruby classes

TOC

Injections

  • Parameterize or serialize user input (including URL query params) before using it
  • Don't pass strings as parameters to Active Records methods. Use arrays or hashes instead
  • Never use user input directly when using the delete_all method
  • Never use user input in system commands
  • Avoid system commands
  • Sanitize ALL hand-written SQL ActiveRecord Sanitization
# bad User.find_by("id = '#{params[:user_id]'") User.delete_all("id = #{params[:user_id]}") User.where(admin: false).group(params[:group]) User.where("name = '#{params[:name]'") # good User.find(id) User.find_by(id: params[:id]) User.find_by_id(params[:id].to_i) # better User.where({ name: params[:name] }) User.where(admin: false).group(:name) User.where("name LIKE ?", "#{params[:search]}%") User.where("name LIKE ?", User.sanitize_sql_like(params[:search]) + "%") 

Cross-site Scripting

By default, when string data is shown in views, it is escaped prior to being sent back to the browser.

  • Never disable ActiveSupport#escape_html_entities_in_json
  • Don't use raw, html_safe, content_tag, or <%==
  • Prefer Markdown over HTML
  • Validate and sanitize user input for Urls and Html (including classes or attributes)
  • Never create templates in code (use ERB, Slim, Haml, etc)
  • Never use render inline or render text
  • Never use unquoted variables in HTML attribute
  • Don't use template variables in script blocks
  • Implement Content Security Policy or use SecureHeaders gem if below Rails v5.2
# bad config.action_view.escape_html_entities_in_json = false <%= raw @user.bio %> <%= @user.bio.html_safe %> <%= link_to "Personal Website", @user.personal_website %> <div class=<%= params[:css_class] %></div> <script>var name = <%= @user.name %>;</script> render inline: "<div>#{@user.name}</div>" # good sanitize(@user.bio, tags: %w(b br em i p strong), attributes: %w()) strip_tags("Strip <i>these</i> tags!") # => Strip these tags! strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>') # => Ruby on Rails validates :instagram, url: true, allow_blank: true # link_to("Instagram", @user.instagram) validates :color, hex_color: true # HexColorValidator # <div style="background-color: <% user.color %>"> 

Authentication and Sessions

  • Use a database based session store
  • Never put sensitive information in the session
  • Set an expiration for the session (Limit: 30 minutes)
  • Limit "Remember Me" functionality to 2 weeks
  • The same timeline can be used for access & refresh tokens
  • Set all cookies and session store as httponly and secure
  • Revalidate cookie values
  • Never store "state" in the session or a cookie
  • Enforce password complexity (min length, no words, etc)
  • Consider captcha on publicly available forms
  • Consider captcha after several failed login attempts
  • Always confirm user emails
  • Require old password to change password (except for forgot password)
  • Expire password reset tokens after 10 minutes
  • Limit password reset emails within a specified timeframe
  • Consider using two-factor authentication (2FA) (required if storing sensitive data)
  • Don't use "Security Questions"
  • Use generic error messages for failed login attempts (Email or password is invalid)
  • add before_action :authenticate_user! to ApplicationController and skip_before_action :authenticate_user! to publicly accessible controllers/actions.
# bad Rails.application.config.session_store :my_custom_store, expire_after: 2.years JWT.encode payload, nil, 'none' # good Rails.application.config.session_store :active_record_store, expire_after: 30.minutes, httponly: true, secure: true cookies[:login] = {value: "user", httponly: true, secure: true} JWT.encode({ data: 'data', exp: Time.now.to_i + 4 * 3600 }, hmac_secret, 'HS256') config.force_ssl = true 

Authorization

  • NEVER do authorization on the frontend
  • Admin interface should be isolated from the user interface
  • Use 2FA on the admin interface
  • Don't use accepts_nested_attributes_for for permissions
  • Prefer policies over querying by association (current_user.posts)
  • Always use policies if using multi-user accounts
# bad @posts = Post.where(user_id: params[:user_id]) @comment = Commend.find_by(id: params[:id]) accepts_nested_attributes_for :permission # good @posts = current_user.posts @posts = policy_scope(Post) @comment = current_user.comments.find_by(id: params[:id]) authorize @post 

Cross-Site Request Forgery

  • If you use cookie-based authentication anywhere, use protect_from_forgery
  • If you use token-based authentication, you don't need protect_from_forgery
# Newer versions of Rails use: config.action_controller.default_protect_from_forgery # Implementation class ApplicationController < ActionController::Base protect_from_forgery with: :exception rescue_from ActionController::InvalidAuthenticityToken do |exception| sign_out_user # destroy the user cookies end ...(rest of file)... end 

Insecure Direct Object Reference or Forceful Browsing

This is basically guessing ids in the path: https://example.com/user/10

  • Use UUIDs, hashids, or a non-guessable id
  • Avoid changing the default primary key (id)
  • Policies can help mitigate this as well
  • Don't let a user-supplied params to determine which view to render
  • Don't show the numerical id in an API call when using a uuid, hashid, etc
Stripe Customer ID = cus_9s6XFG2Qq6Fe7v # don't do this def show render params[:user_supplied_view] end 

Redirects

  • Avoid passing any user-supplied params into redirect_to
  • If you must use user-supplied URLs for redirect_to... sanitize or use an allowlist
  • Validate with regex using \A and \z as anchors, not ^ and $
  • If your needs are complex, use Shopify's redirect_safely gem
# bad redirect_to params[:url] redirect_to URI.parse(params[:url]).path redirect_to URI.parse("#{params[:url]}").host redirect_to "https://yourwebsite.com/" + params[:url] # ok, but not good redirect_to "https://instagram.com/" + params[:ig_username] # good redirect_to user.redirect_url # sanitize beforehand redirect_to AllowList.include?(params[:url]) ? params[:url] : '/' 

Files

  • Avoid user-generated filenames (e.g ../../passwd), assign random names if possible
  • Only allow alphanumeric, underscores, hyphens, and periods
  • Don't process images or videos on your server
  • Always (re)validate on the backend (file size, media type, name, etc.)
  • Process media files asynchronously
  • Use 3rd party scanners if necessary
  • Prefer cloud storage services such as Amazon S3 to directly handle file uploads and storage

Cross-Origin Resource Sharing

  • Use rack-cors gem
  • Unless your API is open to anyone, don't set wildcard as an origin.
# bad Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins'*' resource '*', headers: :any, methods: :any end end # good Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins' http://example.com:80' # regular expressions can be used here resource '*', headers: :any, methods: [:get, :post] end end Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins' http://example.com:80' resource '/orders', :headers => :any, :methods => [:post] resource'/users', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do if Rails.env.development? origins 'localhost:3000', 'localhost:3001', 'https://yourwebsite.com' else origins' https://yourwebsite.com' end resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end 

Data Leaking and Logging

  • NEVER commit credentials, passwords, or keys
  • Use config.filter_parameters for sensitive data (passwords, tokens, etc)
  • Use config.filter_redirect for sensitive location you redirect to
  • Don't use 403 Forbidden for authorized errors (it implies the resource exists)
  • Don't include implementation details in view comments
  • Don't write your own encryption

Misc

  • Encrypt sensitive data at the application layer
  • Don't do this in routes match ':controller(/:action(/:id(.:format)))"
  • Only use https gem sources
  • Use blocks for more than one gem source
  • Never set config.consider_all_requests_local = true in production
  • Separate gems by environment
  • Don't use development-related gems (better_errors) in public-facing environments
  • Don't make non-action controller methods public
  • Use JSON.parse over JSON.load
  • Keep dependencies up-to-date and watch for vulnerabilities
  • Don't store credit card information
  • Avoid user-supplied data in emails to other users
  • Avoid user-created email templates (heavily sanitize or markdown if necessary)
  • Use _html for I18n keys with HTML tags

Additional Resources

About

Ruby on Rails guide, checklist, and tips for security audits and best practices

Topics

Resources

License

Stars

Watchers

Forks

Languages