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

Contributors

Languages