Fork me on GitHub
More Rails: Beyond Backend:

no heading

Security

This guide will give you an introduction to the security features included in Ruby on Rails, how to use them, and how to mess up in spite of all the help the framework is giving you.

By referring to this guide, you will be able to:

  • Use rails's security features
  • Appreciate how hard security is

You can fork the code of the example app. This app is full of security holes. While reading this guide you should work on the app and fix those holes one by one.


Later on this Guide will follow the OWASP Top 10 from 2017 to discuss security features of Ruby on Rails. But first a word of warning:

Rails offers a lot of security features. But all those clever features cannot save you from yourself.

In the example app all the passwords are displayed on "/users". If you as a programmer decide to do that, no framework can prevent it!

1 Regression Tests for Security Problems

Let's use this as an example of how to fix a security problem once you've found it: First we write a test for the problem: rails g integration_test security

require 'test_helper'

class SecurityTest < ActionDispatch::IntegrationTest
  fixtures :users

  test 'users are listed publicly' do
    get '/users'
    assert_response :success
    assert_select 'td', text: users(:one).email

  end

  test 'users password is not shown publicly' do
    get '/users'
    assert_response :success
    assert_select 'td', text: users(:one).password, count: 0
  end
end

When we run this test it fails, because right now passwords are displayed:

Now we change the view to not display the passwords any more. We can run the test to make sure we succeeded.

2 Injection

Injection flaws, such as SQL, NoSQL, OS, and LDAP injection, occur when untrusted data is sent to an interpreter as part of a command or query. The attacker's hostile data can trick the interpreter into executing unintended commands or accessing data without proper authorization. OWASP Wiki

2.1 SQL Injection and ActiveRecord

ActiveRecord will protect against SQL-Injection if you use methods like find and where without string interpolation.

Project.find(42)
# SELECT  "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
# [["id", 42], ["LIMIT", 1]]

Project.where(title: params[:title])
# SELECT "projects".* FROM "projects" WHERE "projects"."title" = $1
# [["title", "Marios Welt"]]

Project.where("publication_date > ?",  1.year.ago)
# SELECT "projects".* FROM "projects" WHERE (publication_date > '2018-06-03 12:15:54.952581')

But if you use string interpolation to build up SQL queries, you open up your application to injection attacks. An example that is vunerable:

@projects = Project.where("title = '#{params[:title]}'")
# SELECT "projects".* FROM "projects" WHERE (title = 'Marios Welt')

If a malicious user enters ' OR ''=' as the name parameter, the resulting SQL query is:

SELECT "projects".* FROM "projects" WHERE (title = '' OR ''='')

As you can see the SQL Fragment was incorporated into the SQL query before the string was handed to ActiveRecord.

So the resulting query returns all records from the projects table. This is because the condition is true for all records.

To test for SQL Injection you can also use an integration test. Check if the result-table has the right number of rows.

  test "should search for users - result table shows 1 row" do
    ...
    assert_select 'table tbody tr', count: 1
  end

  test "should not allow sql injections - result table has not rows" do
    ...
  end

ActiveRecord will use prepared statements by default, unless you configure it not to do that.

production:
  adapter: postgresql
  prepared_statements: false

3 Broken Authentication

Application functions related to authentication and session management are often implemented incorrectly, allowing attackers to compromise passwords, keys, or session tokens, or to exploit other implementation flaws to assume other users' identities temporarily or permanently. OWASP Wiki

Rails comes with basic built in functionality to handle authentication:

  • has_secure_password adds methods to set and authenticate against a BCrypt password to a model.

For most real world projects you will be using a gem:

  • devise to handle typical authentication flows like confimation mail or blocking accounts
  • omniauth to use other authentication providers

We discussed using this in the chapter on Rails Authentication.

3.1 well known passwords

Use the gem pwned to access an API that will tell you if a password is too common and has been featured in password lists:

# Gemfile
gem "pwned"

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password

  validates :password, not_pwned: true
end

Blog article on pwned gem

Friendly reminder: this is how you test if a user can be created:

  test "can create user with password ljkw8723kjasf889r" do
    get "/sign_up"
    assert_response :success

    assert_difference('User.count',1) do
      post "/users", params:{user:{name:"Me Stupid",email:"peter@prayalot.com",password:'ljkw8723kjasf889r',homepage:'https://some.where'}}
    end
    assert_select 'span', text: 'has previously appeared in a data breach and should not be used', count: 0
    follow_redirect!

    assert_select 'li', text: /Me Stupid/
  end

4 Sensitive Data Exposure

Many web applications and APIs do not properly protect sensitive data, such as financial, healthcare, and Peronally Identifiable Information ... Sensitive data may be compromised without extra protection, such as encryption at rest or in transit, and requires special precautions when exchanged with the browser. OWASP Wiki

The OWASP advises: Determine the protection needs of data in transit and at rest. For example, passwords, credit card numbers, health records, personal information and business secrets require extra protection, particularly if that data falls under privacy laws, e.g. EU's General Data Protection Regulation (GDPR), or regulations, e.g. financial data protection such as PCI Data Security Standard (PCI DSS).

4.1 Encryption in the Database

In Rails you can use the attr_encrypted gem to encrypt certain attributes in the database transparently. While choosing to encrypt at the attribute level is the most secure solution, it is not without drawbacks. Namely, you cannot search the encrypted data, and because you can't search it, you can't index it either. You also can't use joins on the encrypted data.

4.2 Removing from the Logfile

By default, Rails logs all requests being made to the web application. You can filter certain request parameters from your log files by appending them to config.filter_parameters in the application configuration. These parameters will be replaced by "[FILTERED]" in the log.

# in initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [:password]

Provided parameters will be filtered out by partial matching regular expression. Rails adds default :password in the appropriate initializer, which will take care of password and password_confirmation.

4.3 Ensuring that HTTPS is used

In the appropriate environment(s) force ssl:

# in config/environments/production.rb

# Force all access to the app over SSL,
# use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true

This will do three things:

  1. Redirect all http requests to their https equivalents.
  2. Set secure flag on cookies rfc 6265 to tell browsers that these cookies must only be sent through https requests.
  3. Add HSTS headers to response. rfc 6797

See this blog article for more details on configuring this

5 XML External Entities (XXE)

Many older or poorly configured XML processors evaluate external entity references within XML documents. External entities can be used to disclose internal files using the file URI handler, internal file shares, internal port scanning, remote code execution, and denial of service attacks. OWASP Wiki

A XEE vunerability in nokogiri was fixed in 2014, but another was found in 2017, and is not completely fixed yet.

Use a service like snyk to learn about vunerable dependencies.

6 Broken Access Control

Restrictions on what authenticated users are allowed to do are often not properly enforced. Attackers can exploit these flaws to access unauthorized functionality and/or data, such as access other users' accounts, view sensitive files, modify other users' data, change access rights, etc. OWASP Wiki

Use all lines of defence on the server to restrict access:

6.2 check user roles and premissions in every controller

The simplest way is to use current_user to control access to models. Instead of simply loading data:

@project = Project.find(params[:id])

Instead, query the user's access rights, too:

@project = current_user.projects.find(params[:id])

For more complex setups with different roles that have different permissions use a gem like cancancan which will let you define access in a declarative way and give you an authorize! method for controllers:

@project = Project.find(params[:id])
authorize! :read, @project

6.3 use UUIDs instead of bigint as id

If you need to have a resource that is available to anyone with the URL (think google docs, doodle), but do not want users to be able to enumerate all possible URLs:

https://my-schedule.at/calendar/17
https://my-schedule.at/calendar/18
https://my-schedule.at/calendar/19 ...

Instead of serial/autocincrement  use of UUID

In postgresql you can use the extentions pgcrypto or uuid-ossp

CREATE EXTENSION pgcrypto; 
CREATE TABLE calendar( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT );

INSERT INTO calendar (name) VALUES 
('meeting on grdp'), 
('security audit');

SELECT * from calendar;
0d60a85e-0b90-4482-a14c-108aea2557aa | meeting on grdp
39240e9f-ae09-4e95-9fd0-a712035c8ad7 | security audit

Rails can handle all this for you:

# in config/application.rb
config.generators do |g|
  g.orm :active_record, primary_key_type: :uuid
end

now your urls will be harder to enumerate:

https://my-schedule.at/calendar/0d60a85e-0b90-4482-a14c-108aea2557aa
https://my-schedule.at/calendar/39240e9f-ae09-4e95-9fd0-a712035c8ad7 ...

6.4 set CORS for your API

Browsers restrict cross-origin HTTP requests initiated by scripts. For example, XMLHttpRequest and the Fetch API follow the same-origin policy. This means that a web application using those APIs can only request HTTP resources from the same origin the application was loaded from, unless the response from the other origin includes the right CORS headers.

cors principle

If you want to make your API available to frontends on other origins you can use the rack-cors gem:

# Gemfile
gem 'rack-cors'

The configuration is done in an initializer:

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3000', '127.0.0.1:3000', 'https://my-frontend.org'
    resource '/api/v1/*',
      methods: %i(get post put patch delete options head),
      max_age: 600
  end

  allow do
    origins '*'
    resource '/public/*', headers: :any, methods: :get
  end
end

7 Security Misconfiguration

Security misconfiguration is a result of insecure default configurations, incomplete or ad hoc configurations, open cloud storage, misconfigured HTTP headers, and verbose error messages containing sensitive information. Not only must all operating systems, frameworks, libraries, and applications be securely configured, but they must be patched/upgraded in a timely fashion. OWASP Wiki

This is espacially relevent if you are running your own virtual machine:

  • upgrade the operating system, apply security patches
  • remove unused components, e.g. a wordpress installation you no longer need
  • upgrade ruby after security problems are fixed

7.1 Use Environment Variables

The files config/database.yml and config/secrets.yml should not be added to the repository - unless you extract out all the secrets into environment variables (as a 12 factor app)

# in config/database.yml

default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

production:
  <<: *default
  database: myapp_production
  username: myapp
  password: <%= ENV['MYAPP_DATABASE_PASSWORD'] %>

7.2 Handling Secrets and Credentials

Rails 5.2 and later generates two files to handle credentials (passwords, api keys, ...):

  • config/credentials.yml.enc to store the credentials within the repo
  • config/master.key or ENV["RAILS_MASTER_KEY"] to read the encryption key from

The master key is never stored in the repo.

To edit stored credentials use rails credentials:edit.

By default, this file contains the application's secret_key_base, but it could also be used to store other credentials such as access keys for external APIs.

The credentials are accessible in the running Rails app via Rails.application.credentials.

For example, with the following decrypted config/credentials.yml.enc:

secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
some_api_key: SOMEKEY

Rails.application.credentials.some_api_key returns SOMEKEY in any environment.

If you want an exception to be raised when some key is blank, use the bang version:

Rails.application.credentials.some_api_key!
# => raises KeyError: :some_api_key is blank

8 Cross-Site Scripting (XSS)

XSS flaws occur whenever an application includes untrusted data in a new web page without proper validation or escaping, or updates an existing web page with user-supplied data using a browser API that can create HTML or JavaScript. XSS allows attackers to execute scripts in the victim's browser which can hijack user sessions, deface web sites, or redirect the user to malicious sites. OWASP Wiki

8.1 Use a Content Security Policy (CSP)

In Rails 5.2 and later you dan configure a Content Security Policy for your application in an initializer. You can configure a global default policy and then override it on a per-resource basis.

Example global policy:

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.default_src :self
  policy.font_src    :self, 'https://fonts.gstatic.com'
  policy.img_src     '*'
  policy.object_src  :none
  policy.script_src  :self, 'https://code.jquery.com'
  policy.style_src   :self, 'https://fonts.googleapis.com'

  # Specify URI for violation reports
  policy.report_uri "/csp-violation-report-endpoint"
end

This automatically forbids all 'unsave-inline' script: <script>-tags in the html code and event-handler-attributes like <button onclick=...>.

To allow certain <script>-tags in your code you can give them a "nonce":

<script nonce="2726c7f26c">
  var inline = 1;  // good javascript
</script>

This must be the same nonce given in the CSP:

Content-Security-Policy: script-src 'nonce-2726c7f26c'

Rails can generate separate nonces for separate sessions automatically, see the Rails Security Guide.

If you want to handle violation reports, you need to set up a model, controller and route as described here.

8.2 Escape for the correct context:

erb automatically escapes for HTML:

<%= @article.title %>

This escaping is not apporpriate for attributes:

<p class=<%= params[:style] %>...</p>

An attacker can insert a space into the style parameter like so: x%22onmouseover=javascript:alert('hacked')

To construct HTML Attributes that are properly escaped it is easiest to use view helpers like tag and content_tag:

<%= content_tag :p, "...", class: params[:style]  %>

In the context of JSON you need to use json_encode:

<script>
  var userdata = <%= raw json_encode(@stuff.to_json) %>
</script>

When building a JSON API use jbuilder or active_model_serializers as described in chapter APIs.

See XSS in the brakeman documentation

9 Insecure Deserialization

Insecure deserialization often leads to remote code execution. Even if deserialization flaws do not result in remote code execution, they can be used to perform attacks, including replay attacks, injection attacks, and privilege escalation attacks. OWASP Wiki

Brakeman will warn about Unsafe Deserialization

10 Using Components with Known Vulnerabilities

Components, such as libraries, frameworks, and other software modules, run with the same privileges as the application. If a vulnerable component is exploited, such an attack can facilitate serious data loss or server takeover. Applications and APIs using components with known vulnerabilities may undermine application defenses and enable various attacks and impacts. OWASP Wiki

There are several tools that check for vulnerabilities in dependencies:

When using script-tags to include javascript (e.g. jquery, bootstrap from a cdn) use Subresource Integrity checks to prevent man in the middle attacks using your javascript.

<script
        src="https://code.jquery.com/jquery-3.3.1.min.js"
        integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
        crossorigin="anonymous"></script>

(This example from jquery also includes the CORS attribte crossorigin set to anonymous. This way no user credentials will every be sent to code.jquery.com).

11 Insufficient Logging&Monitoring

Insufficient logging and monitoring, coupled with missing or ineffective integration with incident response, allows attackers to further attack systems, maintain persistence, pivot to more systems, and tamper, extract, or destroy data. Most breach studies show time to detect a breach is over 200 days, typically detected by external parties rather than internal processes or monitoring. OWASP Wiki

This is really outside the scope of the backend framework.

12 Cross Site Request Forgery (CSRF)

This security problem used to be No 8 on the list, but was no longer listed in the 2017.

A CSRF attack forces a logged-on victim’s browser to send a forged HTTP request, including the victim’s session cookie and any other automatically included authentication information, to a vulnerable web application. This allows the attacker to force the victim’s browser to generate requests the vulnerable application thinks are legitimate requests from the victim. OWASP Wiki

First use GET and POST appropriately. Secondly, a security token in non-GET requests will protect your application from CSRF. Rails can handle this for you:

To protect against all other forged requests, we introduce a required security token that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created Rails applications:

# in app/controller/application_controller.rb

protect_from_forgery with: :exception

This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown.

By default, Rails includes an unobtrusive scripting adapter, which adds a header called X-CSRF-Token with the security token on every non-GET Ajax call. Without this header, non-GET Ajax requests won't be accepted by Rails. When using another library to make Ajax calls, it is necessary to add the security token as a default header for Ajax calls in your library.

Note that cross-site scripting (XSS) vulnerabilities bypass all CSRF protections. XSS gives the attacker access to all elements on a page, so they can read the CSRF security token from a form or directly submit the form.

13 See Also