…and how to fix them 🙂
I have had several opportunities to find and fix various security issues within Ruby on Rails applications over time. Based on my own experience I would like to help you with making Rails apps more secure. At the same time, I hope that you won’t find any of the below issues in any of your apps.
1. Lack of session expiration mechanism
Security issue description
According to the Securing Rails Applications guide:
Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking and session fixation.
Well, even though an infinite session duration seems to be a right approach from user experience perspective (because user stays signed in forever, so she doesn’t have to sign in every time she visits your application) it’s a bad idea. To prevent situations when somebody hijacks user’s session because she forgot to sign out using a computer in a public library, the session should expire as soon a possible.
Solution(s)
The simplest solution is to set an expiration timestamp of the session cookie within the `config/initializers/session_store.rb` initializer. The below line:
Rails.application.config.session_store :cookie_store, expire_after: 12.hours
would set the session cookie to expire automatically 12 hours after creation. This solution is straightforward to be implemented, however, has a major drawback. It sets an expiration timestamp in a user’s browser. Anybody who would gain an access to the session cookie can easily extend the timestamp by modifying the cookie.
To solve the issue in a much more secure way the expiration timestamp should be stored on a server side. This is also a solution proposed in the Securing Rails Application guide:
One possibility is to set the expiry time-stamp of the cookie with the session ID. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safe.
If you use `devise` gem for users authentication in your Ruby on Rails app it has a built-in `Timeoutable` module which takes care of verifying whether a user session has already expired or not. To use it you need to enable it inside a model which represents a user in your application:
class User < ActiveRecord::Base devise :timeoutable end
After that you can set `timeout_in` option of `devise` initializer to a value which fits your needs (by default it’s 30 minutes):
# ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. # After this time the user will be asked for credentials again. # Default is 30 minutes. config.timeout_in = 30.minutes
If you don’t use the gem you can create a `Session` model which would store a user session together with `created_at` & `updated_at` timestamps and take care of removing outdated records. Again, you can find an example in the official guide.
2. Missing a lockout mechanism
Security issue description
How many times a single user can try to sign in into your application before being banned? If your answer is infinitely, that means that you have a security hole. If a user can try many e-mail and password combinations without any consequence, it also means that an attacker can. Preparing a script to make a dictionary or brute-force attack is a matter of minutes today.
A dictionary attack is a guessing attack based on a precompiled list of options like most commonly used passwords.
To fix the issue user should be blocked after providing an incorrect combination of login and password X number of times.
Solution(s)
If you use `devise` gem the solution is as simple as the previous one. There is `Lockable` module which allows blocking a user access after a certain number of attempts. A number of allowed attempts is up to you, but five seems to be a good starting point. You can always tweak the value if you start to receive valid complaints from users.
The module provides two unlocking strategies:
- `:time` which unblocks a user automatically after some configured time.
- `:email` which sends an email to a user when the lock happens, containing a link to unlock an account.
Each of them is much better than none, but again the final decision is up to you. `devise` also make it possible to use both of them at the same time. You can find more details in the official documentation.
If you don’t use the gem you can implement a similar solution by yourself (the code is open 🙂 ) or check if a library you use offers a similar solution.
3. User enumeration / guessable email addresses
Security issue description
Not so obvious, but a serious problem. Please visit your application and go to no-so-often-visited Reset your password page. What would happen if you provided an e-mail address which is not associated with any user of the application?
I hope that’s not a validation error with User with the provided email address does not exist message. Why? For a potential attacker, it provides an easy way to collect email addresses which exist in the system.
It’s an easy-peasy job to prepare or find a ready-to-use script for making millions of request with existing email addresses and based on your application responses determine which one of them exist. Having such a list an attacker can use a security hole described in the previous point and without a lockout mechanism, she can gain access to user accounts.
Solution(s)
An application should respond the same (either it is a JSON response from API or a redirection to a page with some confirmation message) when a user provides email address assigned to one of the application users or a random one. As a result, an attacker won’t be able to collect email addresses of your users.
If you use `devise` gem, there is a configuration option called `paranoid` which according to the code’s comment:
It will change confirmation, password recovery and other workflows to behave the same regardless if the e-mail provided was right or wrong.
If you don’t use `devise` you should adjust your application to behave the same regardless if a user provides her email address or some other.
4. Privilege escalation aka unauthorized access to resources
Security issue description
Such mistakes should not happen, but they simply just happen. Let’s assume that you created a new API endpoint to fetch user’s project based on its ID:
GET https://my-rails-app.com/api/projects/:project_id
You tested it making some cURL requests using IDs of projects assigned to your test user and your application responded with expected JSON payload containing projects’ details. You deployed the endpoint to production finally. But wait! Have you checked what would happen if you made a request for a project assigned to another user?
Boom! You forgot to limit access to only `current_user`’s projects.
There is an excellent sentence summing up this security issue in the official Rails guide:
As a rule of thumb, no user input data is secure, until proven otherwise, and every parameter from the user is potentially manipulated.1
Solution(s)
Always remember about limiting access to as narrow as possible. If you have access to `current_user` method in your application’s controllers the quickest fix is replacing:
Project.find(params[:id])
by:
current_user.projects.find(params[:id])
If you want to have control over resources in an object-oriented way you can choose either `pundit` or `cancan` gem.
I am more familiar with the first one and it has one useful feature that you should always use in a development environment. By adding the below filter in e.g. your main controller that others inherit from:
after_action :verify_authorized
the gem will shout on you if you forget to call its `authorize` method (which limits access to resources basically) in any of controllers’ actions2. Thanks to that you can act even before pushing code to a repository.
If you have played with neither `pundit` nor `cancan` yet I recommend giving them a shot.
5. Allowing users to use weak passwords
Security issue description
The vast majority of our applications’ users do not have access to tools like 1password or KeePass which allow us to generate secure passwords, store them securely and fill them out automatically during every sign in.
An average user chooses passwords that are easy to remember and very often use the same password for every application.
Please don’t allow users to create accounts in your application using passwords like `12345678` or `qwerty`. They make brute-force and dictionary attack way much easier.
Solution(s)
Introduce and apply a password policy.
A password policy is a set of rules designed to enhance computer security by encouraging users to employ strong passwords and use them properly3.
To apply a basic policy just add a custom validation method in your `User` model:
validate :password_complexity def password_complexity return if password.blank? || password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,70}$/ errors.add :password, "Complexity requirement not met. Length should be 8-70 characters and include: 1 uppercase, 1 lowercase, 1 digit and 1 special character" end
The above method taken from Devise’s wiki should be a part of your application’s code as soon as you finish reading this article 🙂
If you want to have something fancier you can take a look at `strong_password` gem, but it may be an overkill 🙂
Summary
There were, are and will be security issues in Ruby on Rails applications.
I hope that none of the above issues are present in an application you develop. If you found something that may be corrected I hope that the proposed solutions will help you fix security holes. Good luck 🙂
Useful links
- The official OWASP Ruby on Rails security checklist
- Rails Security Checklist
- The SaaS CTO Security Checklist
Footnotes
- https://edgeguides.rubyonrails.org/security.html#privilege-escalation
- https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
- https://en.wikipedia.org/wiki/Password_policy