:key: Community-driven Rails Security Checklist (see our GitHub Issues for the newest checks that aren't yet in the README)
This checklist is limited to Rails security precautions and there are many other aspects of running a Rails app that need to be secured (e.g. up-to-date operating system and other software) that this does not cover. Consult a security expert.
One aim for this document is to turn it into a community resource much like the Ruby Style Guide.
BEWARE this checklist is not comprehensive and was originally drafted by a Rails developer with an interest in security - not a security expert - so it may have some problems - you have been warned!
ApplicationController
(and other abstract controllers)
authenticate_user!
)verify_authorized
)verify_policy_scoped
)routes.rb
. It is intentional this duplicates many of the security checks you already perform in the controller callbacks (Devise's authenticate
and authenticated
) (motivations: defence-in-depth, swiss cheese model).routes.rb
are protected with correct authentication and authorization checks. For sensitive engines/Rack apps favor not leaking they are installed at all by responding with 404 to non-logged in admin users.Avoid HTML comments in view templates as these are viewable to clients. Use server-side comments instead:
# bad - HTML comments will be visible to users who "View Source":
<!-- This will be sent to clients -->
<!-- <%= link_to "Admin Site", "https://admin.example.org/login" %> -->
# ok - ERB comments are removed by the server, and so not viewable to clients:
<%# This will _not_ be sent to clients %>
<%#= link_to "Admin Site", "https://admin.example.org/login" %>
Referer
header leaking URL secret tokens to 3rd parties (e.g. password reset URLs can be leaked to CDNs, JS hosted by third parties, other sites you link to) https://robots.thoughtbot.com/is-your-site-leaking-password-reset-links
Avoid exposing sequential IDs (98
, 99
, 100
, ...) which can leak information about your app's usage and assist forced browsing attacks. For example, sequential IDs are often exposed in URLs, form field HTML source, and APIs. Sequential IDs reveal the size and rate at which certain types of data are created in your app. For example, if a competitor signs up to your service and their account page is at path /users/12000
, and they sign up again in a month, and their new account path is /users/13000
, you have leaked that your service gains roughly 1,000 sign-ups per month and has 13,000 accounts total. It is not recommended but some small mitigation can be made by starting IDs at a very large number, however this still leaks the rate of new data creation.
If IDs need to be exposed in URLs, forms, etc., favor less predictable IDs such as UUIDs or hashids instead of sequential IDs. For files consider using a technique like Paperclip's URI Obfuscation to produce unpredictable file paths (URI Obfuscation will need to be used alongside other protections).
Configure Rails model generators to use UUID primary keys by default:
# In config/application.rb
config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end
SecureRandom
or should we favor https://github.com/cryptosphere/sysrandom ? Avoid Rails insecure default where it operates a blocklist and logs most request parameters. A safelist would be preferable. Set up the filter_parameters
config to log no request parameters:
# File: config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [:password]
if Rails.env.production?
MATCH_ALL_PARAMS_PATTERN = /.+/
Rails.application.config.filter_parameters += [MATCH_ALL_PARAMS_PATTERN]
end
Regularly audit what data is captured by log files, 3rd party logging, error catching and monitoring services. You (and your users!) may be surprised at what sensitive information you find. Data stored in log files and 3rd party services can be exploited.
Favor minimal logging.
Consider not archiving logs or regularly purging archived logs stored by you and 3rd parties.
Loofah::XssFoliate
https://github.com/flavorjones/loofah-activerecord)Favor markdown rendering that operates using a safelist of permitted features and forbids rendering arbitrary HTML, especially if you accept markdown input from users.
If using RedCarpet, favor Redcarpet::Render::Safe
over other renderers such as RedCarpet::Render::HTML
# bad
renderer = Redcarpet::Render::HTML.new
# less risky
renderer = Redcarpet::Render::Safe.new
paranoid
mode (except on registration) (source)
Favor padding/increasing the time it takes to initially login and to report failed password attempts so as to mitigate timing attacks you may be unaware of and to mitigate brute force and user enumeration attempts. See how PayPal shows the "loading..." screen for a good few seconds when you first login (should this always be a fixed set amount of time e.g. 5 seconds and error asking user to try again if it takes longer?)(please correct me on this or add detail as this is an assumption I'm making about the reasons why PayPal do this).
Mitigate timing attacks and length leaks on password and other secret checking code https://thisdata.com/blog/timing-attacks-against-string-comparison/
Avoid using secret tokens for account lookup (includes API token, password reset token, etc.). Do not query the database using the token, this is vulnerable to timing attacks that can reveal the secret to an attacker. Use an alternative identifier that is not the token for the query (e.g. username, email, api_locator
).
# bad - timing attack can reveal actual token
user = User.find_by(token: submitted_token)
authenticated = !user.nil?
# less risky
# step 1: find user by an identifier that is *not* the API key, e.g. username, email, api_locator
user = User.find_by(username: submitted_username)
# step 2: compare tokens taking care to mitigate timing attacks and length leaks.
# (NB. favor *not* storing the token in plain text)
authenticated = ActiveSupport::SecurityUtils.secure_compare(
# using digests mitigates length leaks
::Digest::SHA256.hexdigest(user.token),
::Digest::SHA256.hexdigest(submitted_token)
)
sanitize_sql_array
).bundle outdated
results regularly and act as neededhtml_safe
, raw
, etc. usage and review\A
and \z
as regular expression anchors instead of ^
and $
(http://guides.rubyonrails.org/security.html#regular-expressions)ApplicationController
feels like a pain in the neck when writing a public-facing controller that requires no authentication and no authorization checks, you're doing something right.routes.rb
and controller callbacks is a form of duplication but provides better defence.Contributions welcome!