Home

Awesome

Zen Rails Security Checklist

Summary

This document provides a not necessarily comprehensive list of security measures to be implemented when developing a Ruby on Rails application. It is designed to serve as a quick reference and minimize vulnerabilities caused by developer forgetfulness. It does not replace developer training on secure coding principles and how they can be applied.

Describing how each security vulnerability works is outside the scope of this document. Links to external resources containing further information are provided in the corresponding sections of the checklist. Please apply only the suggestions you thoroughly understand.

Please keep in mind that security is a moving target. New vulnerabilities and attack vectors are discovered every day. We suggest you try to keep up to date, for instance, by subscribing to security mailing lists related to the software and libraries you are using.

This checklist is meant to be a community-driven resource. Your contributions are welcome!

Disclaimer: This document does not cover all possible security vulnerabilities. The authors do not take any legal responsibility for the accuracy or completeness of the information herein.

Supported Rails Versions

This document focuses on Rails 4 and 5. Vulnerabilities that were present in earlier versions and fixed in Rails 4 are not included.

<!-- START doctoc generated TOC please keep comment here to allow auto update --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

Table of Contents

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Table of contents generated by DocToc.

The Checklist

Injection

Injection attacks are #1 at the OWASP Top10.

Resources:

Authentication

Broken Authentication and Session Management are #2 at the OWASP Top 10.

Sessions & Cookies

Broken Authentication and Session Management are #2 at the OWASP Top 10.

Resources:

Cross-Site Scripting (XSS)

XSS is #3 at the OWASP Top 10.

Handling User Input
Output Escaping & Sanitization
XSS protection in HAML templates

Resources:

Content Security Policy (CSP)

Resources:

Insecure Direct Object Reference

Resources:

HTTP & TLS

Security-related headers

Memcached Security

Resources:

Authorization (Pundit)

Resources:

Files

File Uploads
File Downloads

Resources:

Cross-Site Request Forgery (CSRF)

Resources:

Cross Origin Resource Sharing (CORS)

You can use rack-cors gem and in config/application.rb specify your configuration (code sample).

Resources:

Sensitive Data Exposure

Credentials

Resources:

Routing, Template Selection, and Redirection

Resources:

Third-party Software

Security Tools

Resources:

Testing

Others

Details and Code Samples

Command Injection example

# User input
params[:shop][:items_ids] # Maybe you expect this to be an array inside a string.
                          # But it can contain something very dangerous like:
                          # "Kernel.exec('Whatever OS command you want')"

# Vulnerable code
evil_string = params[:shop][:items_ids]
eval(evil_string)

If you see a call to eval you must be very sure that you are properly sanitizing it. Using regular expressions is a good way to accomplish that.

# Secure code
evil_string = params[:shop][:items_ids]
secure_string = /\[\d*,?\d*,?\d*\]/.match(evil_string).to_s

eval(secure_string)

Password validation regex

We may implement password strength validation in Devise by adding the following code to the User model.

validate :password_strength

private

def password_strength
  minimum_length = 8
  # Regex matches at least one lower case letter, one uppercase, and one digit
  complexity_regex = /\A(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/
  # When a user is updated but not its password, the password param is nil
  if password.present? &&
    (password.length < minimum_length || !password.match(complexity_regex))
    errors.add :password, 'must be 8 or more characters long, including 
                           at least one lowercase letter, one uppercase
                           letter, and one digit.'
  end
end

Pundit: ensure all actions are authorized

Add the following to app/controllers/application_controller.rb

after_action :verify_authorized, except: :index, unless: :devise_controller?
after_action :verify_policy_scoped, only: :index, unless: :devise_controller?

Add the following to controllers that do not require authorization. You may create a concern for DRY purposes.

after_action_skip :verify_authorized
after_action_skip :verify_policy_scoped

Pundit: only display appropriate records in select boxes

Think of a blog-like news site where users with editor role have access to specific news categories, and admin users have access to all categories. The User and the Category models have an HMT relationship. When creating a blog post, there is a select box for choosing a category. We want editors only to see their associated categories in the select box, but admins must see all categories. We could populate that select box with user.categories. However, we would have to associate all admin users with all categories (and update these associations every time a new category is created). A better approach is to use Pundit Scopes to determine which categories are visible to each user role and use the policy_scope method when populating the select box.

# app/views/posts/_form.html.erb
f.collection_select :category_id, policy_scope(Category), :id, :name

Convert filter_parameters into a whitelist

Developers may forget to add one or more parameters that contain sensitive data to filter_parameters. Whitelists are usually safer than blacklists as they do not generate security vulnerabilities in case of developer forgetfulness. The following code converts filter_parameters into a whitelist.

# config/initializers/filter_parameter_logging.rb
if Rails.env.production?
  # Parameters whose values are allowed to appear in the production logs:
  WHITELISTED_KEYS = %w(foo bar baz)
  
  # (^|_)ids? matches the following parameter names: id, *_id, *_ids
  WHITELISTED_KEYS_MATCHER = /((^|_)ids?|#{WHITELISTED_KEYS.join('|')})/.freeze
  SANITIZED_VALUE = '[FILTERED]'.freeze
  
  Rails.application.config.filter_parameters << lambda do |key, value|
    unless key.match(WHITELISTED_KEYS_MATCHER)
      value.replace(SANITIZED_VALUE)
    end
  end
else
  # Keep the default blacklist approach in the development environment
  Rails.application.config.filter_parameters += [:password]
end

rack-cors configuration

module Sample
  class Application < Rails::Application
    config.middleware.use Rack::Cors do
      allow do
        origins 'someserver.example.com'
        resource %r{/users/\d+.json},
          headers: ['Origin', 'Accept', 'Content-Type'],
          methods: [:post, :get]
      end
    end
  end
end

Throttling Requests

On some pages like the login page, you'll want to throttle your users to a few requests per minute. This prevents bots from trying thousands of passwords quickly.

Rack Attack is a Rack middleware that provides throttling among other features.

Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
  req.params['email'] if req.path == '/login' && req.post?
end

When I18n key ends up with _html

Instead of the following example:

# en.yml
en:
  hello: "Welcome <strong>%{user_name}</strong>!"
<%= t('hello', user_name: current_user.first_name).html_safe %>

Use the next one:

# en.yml
en:
  hello_html: "Welcome <strong>%{user_name}</strong>!"
<%= t('hello_html', user_name: current_user.first_name) %>

HAML: XSS protection

By default,

="<em>emphasized<em>"
!= "<em>emphasized<em>"

compiles to:

&lt;em&gt;emphasized&lt;/em&gt;
<em>emphasized<em>

Authors

Contributing

Contributions are welcome. If you would like to correct an error or add new items to the checklist, feel free to create an issue followed by a PR. See the TODO section for contribution suggestions.

If you are interested in contributing regularly, drop me a line at the above e-mail to become a collaborator.

TODO

References and Further Reading

License

Released under the MIT License.