Checklist of security precautions for Ruby on Rails applications.
MIT License
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.
This document focuses on Rails 4 and 5. Vulnerabilities that were present in earlier versions and fixed in Rails 4 are not included.
Table of contents generated by DocToc.
Injection attacks are #1 at the OWASP Top10.
#{foo}
) to insert user inputted?
character, named bindeval
, system
, syscall
, %x()
,open
, popen<n>
, File.read
, File.write
, and exec
. Using regularResources:
Broken Authentication and Session Management are #2 at the OWASP Top 10.
config.password_length = 8..128
inconfig/initializers/devise.rb
.config.reconfirmable = true
in config/initializers/devise.rb
.config.send_password_change_notification = true
inconfig/initializers/devise.rb
.config.paranoid = true
inconfig/initializers/devise.rb
will protect the confirmable
,recoverable
and unlockable
modules against user enumeration. To protectregisterable
module, add a captcha to the registration page (seebefore_action :authenticate_user!
toApplicationController
andskip_before_action :authenticate_user!
to publicly accessibleconfig/routes.rb
. Requiringauthenticate :user do
blockdevise_parameter_sanitizer.permit
.Broken Authentication and Session Management are #2 at the OWASP Top 10.
secret_key_base
is set. Strengthens cookie encryption andhttponly
. Search the project for cookiehttponly: true
. Example: cookies[:login] = {value: 'user', httponly: true}
. Restricts cookie access to the Rails server. MitigatesResources:
XSS is #3 at the OWASP Top 10.
link_to
(the second argument) will belink_to
allows any scheme for the URL. If using regex,\Ahttps?
. Mitigates XSS attacks such as enteringjavascript:dangerous_stuff()//http://www.some-legit-url.com
as a website URLdata:
payload that is displayed to other users (e.g., in a\A
and \z
to match string^
and $
as anchors. Mitigates XSS[email protected]\n<script>dangerous_stuff();</script>
.
html_safe
or raw
at the view suppresses escaping. Look for calls to thesehtml_safe
and raw
altogether. Most templating==
:<%== params[:query] %>
. For custom scrubbing, seehtml_safe
, it is possible to introduce cross-site scripting into templates<p class=<%= params[:style] %>...</p>
, an attacker can insert a space into</script>
tag as HTML no matterjson_escape
to_json
is overridden or the value is not valid JSON.render inline: ...
. The value passed in will berender inline: "Thanks #{@user.name}!"
. Assuming users can set their own<%= rm -rf / %>
which will executerm -rf /
on the server! This is called Server Side Template Injection and itrender inline: "Thanks <%= @user.name %>"
. Mitigates XSS attacks.
_html
, it will automatically be marked as html safe while the key interpolations will be escaped! See (example code).!=
in Haml and it should be made sure that no!=
notation in Haml works the way<%= raw(…) %>
works in ERB. See (example code).Resources:
Resources:
"/get/post/6"
, for example, but not "/get/post/9"
but the system does not@user = User.find_by(id: params[:user_id])
– which is@user
parameter based on the"current_user"
session variable like this: @user = current_user
.Resources:
config.force_ssl = true
in config/environments/production.rb
. May also be127.0.0.1
. For availability192.168.0.1
, 172.16.0.1
, or 10.0.0.1
. When--listen 127.0.0.1
or --listen 192.168.0.1
.-U 0
whenResources:
authorize
or policy_scope
method (sample code).user.posts
), consider using policy_scope
. See additional details and sampleResources:
../../passwd
.
/etc/passwd
.
Resources:
protect_from_forgery with: :exception
in all controllers used by web views or inApplicationController
.config.action_controller.per_form_csrf_tokens = true
.Resources:
You can use rack-cors
gem and in config/application.rb
specify your
configuration (code sample).
Resources:
config.filter_parameters
atinitializers/filter_parameter_logging.rb
. For added security, considerfilter_parameters
into a whitelist. See sample<%# This comment syntax with ERB %>
instead of HTML comments. Avoids exposure of404 Not Found
status code instead of 403 Forbidden
for authorization errors.www.myapp.com/users/john-doe
and getting a 403
return statusconfig.consider_all_requests_local = true
in the productionconfig.consider_all_requests_local = true
toconfig/environments/development.rb
. Prevents leakage of exceptions andgroup :development, :test do
blockGemfile
. Prevents leakage of exceptions and even REPL accessconfig/master.key
is created when you runrails new
. It's also added to .gitignore
so it doesn't get committed to yourconfig/credentials.yml.enc
file directly. To addbin/rails credentials:edit
. Use a flat format which means youbin/rails secret
andbin/rails credentials:edit
.master.key
securely. You can scp or sftp the file. Upload the keyconfig/master.key
to/path/to/shared/config/master.key
.RAILS_MASTER_KEY
environment variable. In some casesResources:
redirect_to
. If you have no choice, createhttp://www.my-legit-rails-app.com/redirect?to=www.dangeroussite.com
to match ':controller(/:action(/:id(.:format)))'
and make non-action controllerResources:
MediumSecurity
is a good way to stopbundle --trust-policy MediumSecurity
.rubocop
gem and enables security-related rules in the.rubocop.yml
configuration file.Resources:
role
User
model for privilege escalation purposes.
# 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)
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
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
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
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
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
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
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) %>
By default,
="<em>emphasized<em>"
!= "<em>emphasized<em>"
compiles to:
<em>emphasized</em>
<em>emphasized<em>
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.
Released under the MIT License.