Buy my course: Security for Rails Developers.
I recently gave a conference talk at Rails World in Toronto, about the state of security in Rails 8. This article is a written version of the conference talk. If you want to watch the recording of the talk, you can do so here:
Recent security related features
Dependabot GitHub Action
Rails 7.2 introduced a default GitHub Actions file to run Dependabot checks on the repository. If you don’t know what Dependabot is, it is an automation on GitHub to alert about outdated or vulnerable dependencies.
Why is that important?
When a vulnerability is published to any of your dependencies, hackers can use them to exploit your application. In some cases automated attacks are launched within a day of a CVE, but there are also targeted attacks and some of you even share your Gemfile on Twitter, so it is easy to find out what gems are used in apps. And even though the Rails ecosystem is really good in regards to security, there are still vulnerabilties published often. Just to name a few:
Action Text had an XSS vulnerability recently.
Phlex had 3 XSS vulnerabilties published earlier this year(one of them was reported by me).
SidekiqUI had an XSS vulnerability recently.
Since Rails 7.2, you can easily get notified about these issues when they are made public and quickly update. If you are on an older version of Rails, you can just copy the GitHub Actions config from Rails 7.2. Or if you are not on GitHub, you can use bundler-audit to make sure you are notified when a vulnerability is found in one of your dependencies.
Brakeman in the default CI file
This one is also a change from Rails 7.2. New Rails apps are generated with a GitHub CI configuration that runs Brakeman on the codebase.
What is Brakeman? It is a static code analyzer to identify potential security issues in a codebase It is not a silver bullet and won’t find every security issue. But it is highly recommended because it catches silly mistakes. Brakeman can produce false positives, but you can easily ignore those on your CI server.
ActionController::RateLimiting
This is also a Rails 7.2 feature. Action Controller now has a built in rate-limiter.
Why is this important?
Credential stuffing attacks are very popular these days among cyber criminals. There are plenty of leaked password databases and hackers are trying to login to sites with the leaked credentials, exploiting accounts with reused passwords. Just a few recent examples:
23andMe had a big data breach due to a credential stuffing attack recently. Nearly half of their userbase was affected, and not so nicely, they initially blamed it on the customers, saying they should’ve not reused passwords. I believe, developers should try to mitigate these issues, rather than blaming the users.
General Motors also sufferred a credential stuffing attack that allowed the attackers to make purchases on the affected accounts and accessing their details including partial credit card information.
The last example is Roku, more than half a million Roku accounts were hacked in a recent credential stuffing attack.
The new rate-limiter, makes it easy to mitigate such attacks. It is really simple to use it, you just need to add a single line to your authentication controller:
rate_limit to: 10, within: 3.minutes, only: :create
You can set the number of allowed requests with the to
parameter and set the
time window by the within
parameter.
The rate_limit
call behaves like a before_action
callback, so you can use only
or except
to limit which actions do you want the rate limiting to be applied to. You can also use different rate limiting for different actions in the same controller.
rate_limit to: 10, within: 3.minutes, only: :create,
By default it uses the IP address to group requests, but you can pass a callable as the by
parameter if you wish to change that:
rate_limit to: 10, within: 3.minutes, only: :create, by: -> { request.ip }
When the limit is exceeded, by default a 429 Too Many Requests
response is sent, but you can change this behavior by passing a callable as the with
parameter:
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to
root_url, alert: 'Slow your horses!'}
The rate limiter uses the global cache store for storage, but if you want to
separate your rate limiting data from your cache store, you can do so by passing
a store
parameter:
rate_limit to: 10, within: 3.minutes, only: :create, store: ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
Authentication generator
This is something requested many times in the past and from Rails 8 there will be an authentication generator to generate a basic authentication system. You can run the generator with the following command:
bin/rails generate authentication
invoke erb
create app/views/passwords/new.html.erb
create app/views/passwords/edit.html.erb
create app/views/sessions/new.html.erb
create app/models/session.rb
create app/models/user.rb
create app/models/current.rb
create app/controllers/sessions_controller.rb
create app/controllers/concerns/authentication.rb
create app/controllers/passwords_controller.rb
create app/mailers/passwords_mailer.rb
create app/views/passwords_mailer/reset.html.erb
create app/views/passwords_mailer/reset.text.erb
create test/mailers/previews/passwords_mailer_preview.rb
gsub app/controllers/application_controller.rb
route resources :passwords, param: :token
route resource :session
gsub Gemfile
bundle install --quiet
generate migration CreateUsers email_address:string!:uniq password_digest:string! --force
rails generate migration CreateUsers email_address:string!:uniq password_digest:string! --force
invoke active_record
create db/migrate/20240908095945_create_users.rb
generate migration CreateSessions user:references ip_address:string user_agent:string --force
rails generate migration CreateSessions user:references ip_address:string user_agent:string --force
invoke active_record
create db/migrate/20240908095946_create_sessions.rb
I won’t go into too much into the details on this, I will just touch on the
parts I consider the most important to. You will get a simple authentication with
a password based login and email based password reset. Registrations are not in
the generator at the moment. The generated code stores the session an encrypted cookie, and it uses the has_secure_password
, find_by_password_reset_token
, authenticate_by
Rails helpers. It also has a default rate limiting in the Sessions Controller.
It doesn’t have signups, nor password validation or multi-factor authentication at the
moment, but adding these is not too difficult to be honest.
It is a very simple one, but a good starting point and it demonstrates how to
use the built in Rails helpers to build and authentication system.
Updated maintenance policy
“Minor releases will receive security fixes for two years after the first release in its series. For example, if a theoretical 1.1.0 is released on January 1, 2023, it will receive security fixes until January 1, 2025. After that, it will reach its end-of-life.”
“When a release series reaches its end-of-life, it’s your own responsibility to deal with bugs and security issues. We may provide backports of the fixes and merge them, however there will be no new versions released. We recommend to point your application at the stable branch using Git. If you are not comfortable maintaining your own versions, you should upgrade to a supported version.”
So there might be backported fixes, but it is still highly recommended to keep you application up to date.
Add cvv and cvc as default parameters to filter out in new apps
Small change, but it gives me an opportunity to mention an important Rails feature. Rails can filter sensitive parameters from your logs and cvv and cvc is added to the defaults:
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [
:passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
]
If you happen to process credit cards, you should filter out the cvv
and cvc
from your logs and the rest of the card information too. And anything you
consider sensitive should be filtered from the logs, because a malicious actor
might get access to them.
Why choose Rails for security?
At the second part of my talk, I’d like to share why I think that Rails is a good option for an application with high security standards.
There is a tool for almost everything. If you ever completed a security questionnaire, you will be familiar with some of the following.
You need to encrypt data at rest? Active Record Encryption does the heavy lifting. It is easy to use, uses strong encryption. You can choose between determenistic or non-deterministic encryption and there is also built-in key rotation support.
Strong cryptography? It is Built-in. Sessions, encrypted cookies, etcetera all
use strong, modern encryption.
In case you need secure tokens, ActiveRecord::SecureToken does the heavy lifting
and it is so easy to use with the built in find_by_token_for
helpers
You need to prevent credential stuffing/bruteforce attacks? As I mentioned earlier, there is a built-in rate-limiter in Rails now and if that doesn’t meet your requirements, there is the rack-attack gem too.
You need to filter sensitive information from your logs? I also mentioned this one before, the built in parameter filtering makes this very simple.
You need to audit what developers access in production? There are multiple gems to keep an audit log of Active Record models, for instance PaperTrail. You should store those in a write only table though to make sure they are tamper proof. In the Rails console, you can use console1984 and audits1984 to track what has been accessed by who. This should also store audit logs in a write only table.
You need to run static code analyses? Static code analyses can help to catch vulnerabilties before they hit production. For a Rails app you can use Brakeman or Spektr, a gem I wrote, although the target audience is penetration testers, it is not as smooth to run it on CI as Brakeman.
Vulnerable dependencies? Monitoring vulnerable dependcies is very easy with Rails. The earlier mentioned Dependendabot is integrated into GitHub but there is also bundler audit as a great alternative.
Need a Content Security Policy? A content security policy can specify what kind of javascript can be executed in the browser and you should use one. Configuring it is very simple with Rails as you can see it in the example.
Do you need to handle potential open redirects? Rails has a built in mechanism and can raise an exception that you can rescue and create a custom flow.
To sum it up, I believe Rails is one of the best options to develop a secure web application.
Or follow me on Twitter
I run an indie startup providing vulnerability scanning for your Ruby on Rails app.
It is free to use at the moment, and I am grateful for any feedback about it.If you would like to give it a spin, you can do it here: Vulnerability Scanning for your Ruby on Rails app!