This document provides a list of security standards to be followed while developing. It is designed to serve as a quick reference and minimize vulnerabilities caused by developer forgetfulness.
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 document focuses on Rails 5.2.
- The Checklist
- Injection
- Authentication
- Sessions & Cookies
- Authorization
- Cross-Site Scripting (XSS)
- Insecure Direct Object Reference
- HTTP & TLS
- Memcached Security
- Files
- Cross-Site Request Forgery (CSRF)
- Cross Origin Resource Sharing (CORS)
- Encryption & Hashing
- Sensitive Data Exposure
- Routing, Template Selection, and Redirection
- Third-party Software
- Security Tools
- Testing
- Others
- Details and Code Samples
- Authors
- References and Further Reading
Table of contents generated by DocToc.
Injection attacks are #1 at the OWASP Top10.
- Don’t use standard Ruby interpolation (
#{foo}
) to insert user inputted strings into ActiveRecord or raw SQL queries. Use the?
character, named bind variables or the ActiveRecord::Sanitization methods to sanitize user input used in DB queries. Another basic example:User.where(email: params[:email])
is safe,User.where("email = #{params[:email]}")
is not safe. Refer to this resource for a deeper dive into SQL injection exploits. Mitigates SQL injection attacks. - Don't pass user inputted strings to methods capable of evaluating
code or running O.S. commands such as
eval
,system
,syscall
,%x()
,open
,popen<n>
,File.read
,File.write
, andexec
. Using regular expressions is a good way to sanitize it (code sample). Mitigates command injection attacks.
Resources:
- Ruby on Rails Security Guide - SQL Injection
- Rails SQL Injection Examples
- Step Up the Security of Your Rails App | Part 1
Broken Authentication and Session Management are #2 at the OWASP Top 10.
- Lock accounts after multiple failed login attempts. Mitigates brute-force attacks.
- Require users to input their old password on password change. Mitigates unauthorized password changes on session hijacking, CSRF or when a user forgets to log out and leaves the PC or mobile device unattended.
- Expire the session at log out and expire old sessions at every successful login. Mitigates CSRF, session hijacking and session fixation attacks by reducing their time-frame.
- Expire sessions after a period of inactivity (e.g., 30 minutes). Mitigates CSRF, session hijacking and session fixation attacks by reducing their time-frame.
- Notify user via email on password change. Does not prevent an attacker from changing the victim's password, but warns the victim so he can contact the system administrator to revoke the attacker's access.
- Use generic error messages such as "Invalid email or password" instead of specifying which part (e-mail or password) is invalid. Mitigates user enumeration and brute-force attacks.
- Ensure all non-public controllers/actions require authentication. Avoid unauthorized access due to developer forgetfulness.
- Consider using two-factor authentication (2FA) as provided by Authy. Provides a highly effective extra layer of authentication security.
- Consider requiring authentication in
config/routes.rb
. Requiring authentication in both controllers and routes may not be DRY, but such redundancy provides additional security (see Defense in depth). - Consider limiting the number of simultaneous sessions per account. May reduce application exposure on account compromise (e.g. leaked passwords).
- Avoid implementing "security questions" such as "What is your mother's maiden name?" as their answers may be reused across multiple sites and easily found by means of social engineering. See this article.
- If using role-based access control (RBAC), do not include the role attribute in the strong parameters of the controller(s) used for user registration and profile editing. Prevent malicious users from assigning admin role to themselves.
- Consider restricting administrator access by IP. If the client's IP is dynamic, restrict by IP block/ASN or by country via IP geolocation.
Broken Authentication and Session Management are #2 at the OWASP Top 10.
- Don't store data such as money/point balances or user privileges in a cookie or a CookieStore Session. Store it in the database instead. Mitigates replay attacks.
- Always using encrypted cookies. This is the default behavior in
Rails 5+ whensecret_key_base
is set. Strengthens cookie encryption and mitigates multiple attacks involving cookie tampering. - Unless your JavaScript frontend needs to read cookies generated by the
Rails server, set all cookies as
httponly
. Search the project for cookie accessors and addhttponly: true
. Example:cookies[:login] = {value: 'user', httponly: true}
. Restricts cookie access to the Rails server. Mitigates attackers from using the victim's browser JavaScript to steal cookies after a successful XSS attack.
Resources:
- Use CanCanCan to perform any type of authorization checks for users.
- Use proper scoping when using active record. Instead of
@project = Project.find(params[:id])
, you should use@project = @current_user.projects.find(params[:id])
. If the project doesn't belong to the user, the latter code will return nil.
XSS is #3 at the OWASP Top 10.
- Always validate user input that may eventually be displayed to other users. Attempting to blacklist characters, strings or sanitize input tends to be ineffective (see examples of how to bypass such blacklists). A whitelisting approach is usually safer. Mitigates multiple XSS attacks.
- Consider using the loofah-activerecord gem to scrub your model attribute values. Mitigates multiple XSS attacks.
- If you must create links from user inputted URLs, be sure to validate
them. In particular, it should be possible to limit URL schemes to http/https
in nearly all cases. The URL passed to
link_to
(the second argument) will be HTML escaped. However,link_to
allows any scheme for the URL. If using regex, ensure that the string begins with the expected protocol(s), as in\Ahttps?
. Mitigates XSS attacks such as enteringjavascript:dangerous_stuff()//http://www.some-legit-url.com
as a website URL or a dangerousdata:
payload that is displayed to other users (e.g., in a user profile page). - When using regex for input validation, use
\A
and\z
to match string beginning and end. Do not use^
and$
as anchors. Mitigates XSS attacks that involve slipping JS code after line breaks, such as[email protected]\n<script>dangerous_stuff();</script>
. - Do not trust validations implemented at the client (frontend) as most implementations can be bypassed. Always (re)validate at the server.
- Escape all HTML output. Rails does that by default, but calling
html_safe
orraw
at the view suppresses escaping. Look for calls to these methods in the entire project, check if you are generating HTML from user-inputted strings and if those strings are effectively validated. Note that there are dozens of ways to evade validation. If possible, avoid callinghtml_safe
andraw
altogether. Most templating libraries also provide a way of skipping escaping. ERB uses the double==
:<%== params[:query] %>
. For custom scrubbing, see ActionView::Helpers::SanitizeHelper Mitigates XSS attacks. - Always enclose attribute values with double quotes. Even without
html_safe
, it is possible to introduce cross-site scripting into templates with unquoted attributes. In the following code<p class=<%= params[:style] %>...</p>
, an attacker can insert a space into the style parameter and suddenly the payload is outside the attribute value and they can insert their own payload. And when a victim mouses over the paragraph, the XSS payload will fire. Mitigates XSS attacks. - Rendering JSON inside of HTML templates is tricky. You can't just HTML
escape JSON, especially when inserting it into a script context, because
double-quotes will be escaped and break the code. But it isn't safe to not
escape it, because browsers will treat a
</script>
tag as HTML no matter where it is. The Rails documentation recommends always usingjson_escape
just in caseto_json
is overridden or the value is not valid JSON. Mitigates XSS attacks. - Be careful when using
render inline: ...
. The value passed in will be treated like an ERB template by default. Take a look at this code:render inline: "Thanks #{@user.name}!"
. Assuming users can set their own name, an attacker might set their name to<%= rm -rf / %>
which will executerm -rf /
on the server! This is called Server Side Template Injection and it allows arbitrary code execution (RCE) on the server. If you must use an inline template treat all input the same as you would in a regular ERB template:render inline: "Thanks <%= @user.name %>"
. Mitigates XSS attacks. - Avoid sending user inputted strings in e-mails to other users. Attackers may enter a malicious URL in a free text field that is not intended to contain URLs and does not provide URL validation. Most e-mail clients display URLs as links. Mitigates XSS, phishing, malware infection and other attacks.
- Be careful when using
!=
in Haml and it should be made sure that no user data is rendered unescaped. The!=
notation in Haml works the way<%= raw(…) %>
works in ERB. See (example code).
Resources:
- Ruby on Rails Security Guide - XSS
- OWASP XSS Filter Evasion Cheat Sheet
- OWASP Ruby on Rails Cheatsheet - Cross-site Scripting (XSS)
- Plataformatec Blog - The new HTML sanitizer in Rails 4.2
- Brakeman Pro - Cross-Site Scripting in Rails
- Preventing security issues in Rails
- An IDOR issue arises when the user is supposed to have access to url
"/get/post/6"
, for example, but not"/get/post/9"
but the system does not properly check those permissions. And if we change "6" in the URL, what happens? We can see the data of all users. This may be due to the fact that the data was generated as follows:@user = User.find_by(id: params[:user_id])
– which is basically getting the ID from the GET parameter in the URL. Instead a more secure way of doing this is setting the@user
parameter based on the"current_user"
session variable like this:@user = current_user
.
Resources:
- Force HTTPS over TLS (formerly known as SSL). Set
config.force_ssl = true
inconfig/environments/production.rb
. May also be done in a TLS termination point such as a load balancer, Nginx or Passenger Standalone. Mitigates man-in-the-middle and other attacks. - Use the SSL Server Test tool from Qualys SSL
Lab to check the grade of your TLS
certificate. Be sure to use the strongest (yet widely compatible) protocols
and cipher suites, preferably with Ephemeral Diffie-Hellman support. The
Mozilla SSL Configuration Generator can give you some suggestions. Mitigates multiple SSL/TLS-related attacks such as BEAST and POODLE. - Rate-limiting incoming HTTP requests, as implemented by the rack-attack and rack-throttle gems. See sample code. Mitigates web scraping, HTTP floods, and other attacks.
- Use the Secure Headers gem. Mitigates several attacks.
- Obfuscate the web server banner string. In other words, hide your web server name and version. Mitigates HTTP fingerprinting, making it harder for attackers to determine which exploits may work on your web server.
- Use a firewall. Memcached needs to be accessible from your other servers but there's no reason to expose it to the internet. In short, only your other production servers have access to your production memcached servers. This alone would prevent your server from being used in an attack. Memcached out of the box doesn't use authentication so anyone who can connect to your server will be able to read your data.
- Listen on a private interface. If you're running one server for your Rails
application and memcached, you should listen on
127.0.0.1
. For availability reasons, you shouldn't have 1 server in production anyway. For staging and test environments, follow this rule. For production setups where you have multiple Rails servers that need to connect to memcached, use the private IP of the server. This is something like192.168.0.1
,172.16.0.1
, or10.0.0.1
. When you start memcached, use--listen 127.0.0.1
or--listen 192.168.0.1
. - Disable UDP. It is enabled by default. To disable UDP, use
-U 0
when starting memcached.
Resources:
- Avoid using user controlled filenames. If possible, assign "random"
names to uploaded files when storing them in the OS. If not possible,
whitelist acceptable characters. It is safer to deny uploads with invalid
characters in the filenames than to attempt to sanitize them.
Mitigates Directory Traversal Attacks such as attempting to overwrite
system files by uploading files with names like
../../passwd
. - Avoid using libraries such as ImageMagick to process images and videos on your server. If possible, use an image/video processing service such as Transloadit, Cloudinary, or imgix. Mitigates multiple image/video processing related vulnerabilities such as these.
- Process uploaded files asynchronously. If not possible, implement per-client rate limiting. Mitigates DoS Attacks that involve overloading the server CPU by flooding it with uploads that require processing.
- Validate files before processing. Mitigates DoS Attacks such as image bombs.
- Whitelist acceptable file extensions and acceptable Media Types (formerly known as MIME types). Validating file extensions without checking their media types is not enough as attackers may disguise malicious files by changing their extensions. Mitigates the upload of dangerous file formats such as shell or Ruby scripts.
- Limit file size. Mitigates against DoS attacks involving the upload of very large files.
- Consider uploading directly from the client (browser) to S3 or a similar cloud storage service. Mitigates multiple security issues by keeping uploaded files on a separate server than your Rails application.
- If allowing uploads of malware-prone files (e.g., exe, msi, zip, rar, pdf), scan them for viruses/malware. If possible, use a third party service to scan them outside your server. Mitigates server infection (mostly in Windows servers) and serving infected files to other users.
- If allowing upload of archives such as zip, rar, and gz, validate
the target path, estimated unzip size and media types of compressed files
before unzipping. Mitigates DoS attacks such as zip bombs, zipping
malicious files in an attempt to bypass validations, and overwriting of system
files such as
/etc/passwd
.
- Do not allow downloading of user-submitted filenames and paths. If not possible, use a whitelist of permitted filenames and paths. Mitigates the exploitation of directory traversal vulnerabilities to download sensitive files.
Resources:
- Enforce CSRF protection by setting
protect_from_forgery with: :exception
in all controllers used by web views or inApplicationController
. - Use HTTP verbs in a RESTful way. Do not use GET requests to alter the state of resources. Mitigates CSRF attacks.
- Up to Rails 4, there was a single CSRF token for all forms, actions, and
methods. Rails 5 implements per-form CSRF tokens, which are only valid for a
single form and action/method. Enable it by setting
config.action_controller.per_form_csrf_tokens = true
.
Resources:
- Occasionally the need to share some resources across many domains appears. For example, you want to upload a file using AJAX request and send it to the other app. The receiving side should specify a whitelist of domains that are allowed to make those requests. There are few HTTP headers that control that.
You can use rack-cors
gem and in config/application.rb
specify your
configuration (code sample).
Resources:
- Always use the most up-to-date encryption algorithms for encrypting data. Use a salt and IV when possible. Currently we require AES-256 at a minimum.
- When decryption is not required use hashing to protect data. Bcrypt, and SHA-256 are the currently required minimums.
- Avoid storing sensitive data such as credit cards, tax IDs and third-party authentication credentials in your application. If not possible, ensure that all sensitive data is encrypted at rest (in the DB) and in transit (use HTTPS over TLS). Mitigate theft/leakage of sensitive data.
- Do not log sensitive data such as passwords and credit card numbers. You
may include parameters that hold sensitive data in
config.filter_parameters
atinitializers/filter_parameter_logging.rb
. For added security, consider convertingfilter_parameters
into a whitelist. See sample code. Prevents plain-text storage of sensitive data in log files. - HTML comments are viewable to clients and should not contain details that
can be useful to attackers. Consider using server-side comments such as
<%# This comment syntax with ERB %>
instead of HTML comments. Avoids exposure of implementation details. - Avoid exposing numerical/sequential record IDs in URLs, form HTML source and APIs. Consider using slugs (A.K.A. friendly IDs, vanity URLs) to identify records instead of numerical IDs, as implemented by the friendly_id gem. Additional benefits include SEO and better-looking URLs. Mitigates forced browsing attacks and exposure of metrics about your business, such as the number of registered users, number of products on stock, or number of receipts/purchases.
- If using slugs instead of numerical IDs for URLs, consider returning a
404 Not Found
status code instead of403 Forbidden
for authorization errors. Prevents leakage of attribute values used to generate the slugs. For instance, visitingwww.myapp.com/users/john-doe
and getting a403
return status indicates the application has a user named John Doe.* - Do not set
config.consider_all_requests_local = true
in the production environment. If you need to setconfig.consider_all_requests_local = true
to use the better_errors gem, do it onconfig/environments/development.rb
. Prevents leakage of exceptions and other information that should only be accessible to developers. - Don't install development/test-related gems such as
better_errors and
web-console in the production
environment. Place them within a
group :development, :test do
block in theGemfile
. Prevents leakage of exceptions and even REPL access if using better_errors + web-console.
- The encryption key, located on
config/master.key
is created when you runrails new
. It's also added to.gitignore
so it doesn't get committed to your repository. Mitigates credential leaks/theft. - Don't edit the
config/credentials.yml.enc
file directly. To add credentials, runbin/rails credentials:edit
. Use a flat format which means you don't have to put development or production anymore. Mitigates credential leaks/theft. - If you want to generate a new secret key base run,
bin/rails secret
and add that to your credentials by runningbin/rails credentials:edit
. - Upload
master.key
securely. You can scp or sftp the file. Upload the key to a shared directory. Shared here means shared between releases, not a shared filesystem. On each deploy, you symlinkconfig/master.key
to/path/to/shared/config/master.key
. - If you need to give a developer a copy of the key, never send it via email (unless you're using encrypted emails which most of us don't!) You can use a password manager because they use encryption.
- Put the key on the
RAILS_MASTER_KEY
environment variable. In some cases where you can't upload a file, this is the only option. Even though this is convenient, make sure you know the risks of using environment variables. The risks can be mitigated, but if you can upload master.key then use that option.
Resources:
- Don't perform URL redirection based on user inputted strings. In other
words, don't pass user input to
redirect_to
. If you have no choice, create a whitelist of acceptable redirect URLs or limit to only redirecting to paths within your domain (example code). Mitigates redirection to phishing and malware sites. Prevent attackers from providing URLs such ashttp://www.my-legit-rails-app.com/redirect?to=www.dangeroussite.com
to victims. - Do not use a user inputted string to determine the name of the template or view to be rendered. Prevents attackers from rendering arbitrary views such as admin-only pages.
- Avoid "catch-all" routes such as
match ':controller(/:action(/:id(.:format)))'
and make non-action controller methods private. Mitigates unintended access to controller methods.
Resources:
- Apply the latest security patches in the OS frequently. Pay special attention to internet-facing services such as application servers (Passenger, Puma, Unicorn), web servers (Nginx, Apache, Passenger Standalone) and SSH servers.
- Update Ruby frequently.
- Watch out for security vulnerabilities in your gems. Run bundler-audit frequently or use a service like Snyk, Gemnasium (both free for open-source development) or Appcanary.
- Ensure that vulnerability alerts are enabled on GitHub for each repository.
- Include security tests in your test suite. Look at OWASP's RailsGoat application for examples of security-related Capybara specs. Raises additional security awareness and mitigates security-related regressions.
- Create security tests in pairs: one for the access denied scenario and another for the access granted scenario.
- When using TDD, consider implementing authentication in the early stages of development, as it tends to break multiple preexisting tests.
- Use strong parameters in the controllers.
Mitigates mass assignment attacks such as overwriting the
role
attribute of theUser
model for privilege escalation purposes. - Implement Captcha or Negative Captcha on any publicly exposed forms. reCAPTCHA is a great option, and there is a gem that facilitates Rails integration. Other options are the rucaptcha and negative-captcha gems. Mitigates automated SPAM (spambots).
# 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
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
By default,
="<em>emphasized<em>"
!= "<em>emphasized<em>"
compiles to:
<em>emphasized</em>
<em>emphasized<em>
- Bruno Facca - LinkedIn - Email: bruno at facca dot info