1. Introduction
Web application frameworks are made to help developers build web applications. Some of them also help you with securing the web application. In fact one framework is not more secure than another: If you use it correctly, you will be able to build secure apps with many frameworks. Ruby on Rails has some clever helper methods, for example against SQL injection, so this is hardly a problem.
In general there is no such thing as plug-n-play security. Security depends on the people using the framework, and sometimes on the development method. And it depends on all layers of a web application environment: The back-end storage, the web server, and the web application itself (and possibly other layers or applications).
The Gartner Group, however, estimates that 75% of attacks are at the web application layer, and found out "that out of 300 audited sites, 97% are vulnerable to attack". This is because web applications are relatively easy to attack, as they are simple to understand and manipulate, even by the lay person.
The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment, or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at.
In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs, and make updating and security checks a habit (check the Additional Resources chapter). It is done manually because that's how you find the nasty logical security problems.
2. Authentication
Authentication is often one of the first features implemented in a web application. It serves as the foundation for securing user data and is part of most modern web applications.
Starting with version 8.0, Rails comes with a default authentication generator, which provides a solid starting point for securing your application by only allowing access to verified users.
The authentication generator adds all of the relevant models, controllers, views, routes, and migrations needed for basic authentication and password reset functionality.
To use this feature in your application, you can run bin/rails generate
authentication. Here are all of the files the generator modifies and new files
it adds:
$ 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/20241010215312_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/20241010215314_create_sessions.rb
As shown above, the authentication generator modifies the Gemfile to add the
bcrypt gem. The generator uses
the bcrypt gem to create a hash of the password, which is then stored in the
database (instead of the plain-text password). As this process is not
reversible, there's no way to go from the hash back to the password. The hashing
algorithm is deterministic though, so the stored password is able to be compared
with the hash of the user-inputted password during authentication.
The generator adds two migration files for creating user and session tables.
Next step is to run the migrations:
$ bin/rails db:migrate
Then, if you visit /session/new in your browser (you will see this route has
been added in routes.rb), you'll see a form that accepts an email and a
password with "sign in" button. This form routes to the SessionsController
which was added by the generator. If you provide an email/password for a user
that exists in the database, you will be able to successfully authenticate with
those credentials and log in to the application.
After running the Authentication generator, you do need to implement your
own sign up flow and add the necessary views, routes, and controller actions.
There is no code generated that creates new user records and allows users to
"sign up" in the first place. This is something you'll need to wire up based on
the requirements of your application.
Here is a list of modified files:
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Gemfile
modified: Gemfile.lock
modified: app/controllers/application_controller.rb
modified: config/routes.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/controllers/concerns/authentication.rb
app/controllers/passwords_controller.rb
app/controllers/sessions_controller.rb
app/mailers/passwords_mailer.rb
app/models/current.rb
app/models/session.rb
app/models/user.rb
app/views/passwords/
app/views/passwords_mailer/
app/views/sessions/
db/migrate/
db/schema.rb
test/mailers/previews/
2.1. Reset Password
The authentication generator also adds reset password functionality. You can see
a "forgot password?" link on the "sign in" page. Clicking that link navigates to
the /passwords/new path and routes to the passwords controller. The new
method of the PasswordsController class runs through the flow for sending a
password reset email.
The link is valid for 15 minutes by default, but this can be configured with
has_secure_password.
The mailers for reset password are also set up by the generator at
app/mailers/password_mailer.rb and render the following email to send to the
user:
# app/views/passwords_mailer/reset.html.erb
<p>
You can reset your password within the next 15 minutes on
<%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
</p>
2.2. Implementation Details
This section covers some of the implementation details around the authentication
flow added by the authentication generator: The has_secure_password method,
the authenticate_by method, and the Authentication concern.
2.2.1. has_secure_password
The
has_secure_password
method is added to the user model and takes care of storing a hashed password
using the bcrypt algorithm:
class User < ApplicationRecord
has_secure_password
has_many :sessions, dependent: :destroy
normalizes :email_address, with: -> e { e.strip.downcase }
end
has_secure_password adds the following validations automatically:
- Password must be present on creation
- Password length should be less than or equal to 72 bytes
- Confirmation of password (using a XXX_confirmation attribute)
However it doesn't validate the minimum length or the complexity of the password, you need to define validation for those yourself.
2.2.2. authenticate_by
The
authenticate_by
method is used in the SessionsController while creating a new session to
validate that the credentials provided by the user match the credentials stored
in the database (e.g. password) for that user:
class SessionsController < ApplicationController
def create
if user = User.authenticate_by(params.permit(:email_address, :password))
start_new_session_for user
redirect_to after_authentication_url
else
redirect_to new_session_url, alert: "Try another email address or password."
end
end
# ...
end
If the credentials are valid, a new Session is created for that user.
2.2.3. Session Management
The core functionality around session management is implemented in the
Authentication controller concern, which is included by the
ApplicationController in your application. You can explore details of the
authentication
concern
in the source code.
One method to note in the Authentication concern is authenticated?, a helper
method available in view templates. You can use this method to conditionally
display links/buttons depending on whether a user is currently authenticated.
For example:
<% if authenticated? %>
<%= button_to "Sign Out", session_path, method: :delete %>
<% else %>
<%= link_to "Sign In", new_session_path %>
<% end %>
You can find all of the details for the Authentication generator in the Rails source code. You are encouraged to explore the implementation details and not treat authentication as a black box.
With the authentication generator configured as above, your application is ready for a more secure user authentication and password recovery process in just a few steps.
3. Sessions
This chapter describes some particular attacks related to sessions, and security measures to protect your session data.
3.1. What are Sessions?
Sessions enable the application to maintain user-specific state, while users interact with the application. For example, sessions allow users to authenticate once and remain signed in for future requests.
Most applications need to keep track of state for users that interact with the application. This could be the contents of a shopping basket, or the user id of the currently logged in user. This kind of user-specific state can be stored in the session.
Rails provides a session object for each user that accesses the application. If the user already has an active session, Rails uses the existing session. Otherwise a new session is created.
Read more about sessions and how to use them in Action Controller Overview Guide.
3.2. Session Hijacking
Stealing a user's session ID lets an attacker use the web application in the victim's name.
Many web applications have an authentication system: a user provides a username and password, the web application checks them and stores the corresponding user id in the session hash. From now on, the session is valid. On every request the application will load the user, identified by the user id in the session, without the need for new authentication. The session ID in the cookie identifies the session.
Hence, the cookie serves as temporary authentication for the web application. Anyone who seizes a cookie from someone else, may use the web application as this user - with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures:
Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN, it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to provide a secure connection over SSL. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file:
config.force_ssl = trueMost people don't clear out the cookies after working at a public terminal. So if the last user didn't log out of a web application, you would be able to use it as this user. Provide the user with a log-out button in the web application, and make it prominent.
Many cross-site scripting (XSS) exploits aim at obtaining the user's cookie. You'll read more about XSS later.
Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later.
3.3. Session Storage
Rails uses ActionDispatch::Session::CookieStore as the default session storage.
Learn more about other session storages in Action Controller Overview Guide.
Rails CookieStore saves the session hash in a cookie on the client-side.
The server retrieves the session hash from the cookie and
eliminates the need for a session ID. That will greatly increase the
speed of the application, but it is a controversial storage option and
you have to think about the security implications and storage
limitations of it:
Cookies have a size limit of 4 kB. Use cookies only for data which is relevant for the session.
Cookies are stored on the client-side. The client may preserve cookie contents even for expired cookies. The client may copy cookies to other machines. Avoid storing sensitive data in cookies.
Cookies are temporary by nature. The server can set expiration time for the cookie, but the client may delete the cookie and its contents before that. Persist all data that is of more permanent nature on the server side.
Session cookies do not invalidate themselves and can be maliciously reused. It may be a good idea to have your application invalidate old session cookies using a stored timestamp.
Rails encrypts cookies by default. The client cannot read or edit the contents of the cookie, without breaking encryption. If you take appropriate care of your secrets, you can consider your cookies to be generally secured.
The CookieStore uses the
encrypted
cookie jar to provide a secure, encrypted location to store session
data. Cookie-based sessions thus provide both integrity as well as
confidentiality to their contents. The encryption key, as well as the
verification key used for
signed
cookies, is derived from the secret_key_base configuration value.
Secrets must be long and random. Use bin/rails secret to get new unique secrets.
Learn more about managing credentials later in this guide
It is also important to use different salt values for encrypted and signed cookies. Using the same value for different salt configuration values may lead to the same derived key being used for different security features which in turn may weaken the strength of the key.
In test and development applications get a secret_key_base derived from the app name. Other environments must use a random key present in config/credentials.yml.enc, shown here in its decrypted state:
secret_key_base: 492f...
If your application's secrets may have been exposed, strongly consider changing them. Note that changing secret_key_base will expire currently active sessions and require all users to log in again. In addition to session data: encrypted cookies, signed cookies, and Active Storage files may also be affected.
3.4. Rotating Encrypted and Signed Cookies Configurations
Rotation is ideal for changing cookie configurations and ensuring old cookies aren't immediately invalid. Your users then have a chance to visit your site, get their cookie read with an old configuration and have it rewritten with the new change. The rotation can then be removed once you're comfortable enough users have had their chance to get their cookies upgraded.
It's possible to rotate the ciphers and digests used for encrypted and signed cookies.
For instance to change the digest used for signed cookies from SHA1 to SHA256, you would first assign the new configuration value:
Rails.application.config.action_dispatch.signed_cookie_digest = "SHA256"
Now add a rotation for the old SHA1 digest so existing cookies are seamlessly upgraded to the new SHA256 digest.
Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
cookies.rotate :signed, digest: "SHA1"
end
Then any written signed cookies will be digested with SHA256. Old cookies that were written with SHA1 can still be read, and if accessed will be written with the new digest so they're upgraded and won't be invalid when you remove the rotation.
Once users with SHA1 digested signed cookies should no longer have a chance to have their cookies rewritten, remove the rotation.
While you can set up as many rotations as you'd like it's not common to have many rotations going at any one time.
For more details on key rotation with encrypted and signed messages as
well as the various options the rotate method accepts, please refer to
the
MessageEncryptor API
and
MessageVerifier API
documentation.
3.5. Replay Attacks for CookieStore Sessions
Another sort of attack you have to be aware of when using CookieStore is the replay attack.
It works like this:
- A user receives credits, the amount is stored in a session (which is a bad idea anyway, but we'll do this for demonstration purposes).
- The user buys something.
- The new adjusted credit value is stored in the session.
- The user takes the cookie from the first step (which they previously copied) and replaces the current cookie in the browser.
- The user has their original credit back.
Including a nonce (a random value) in the session solves replay attacks. A nonce is valid only once, and the server has to keep track of all the valid nonces. It gets even more complicated if you have several application servers. Storing nonces in a database table would defeat the entire purpose of CookieStore (avoiding accessing the database).
The best solution against it is not to store this kind of data in a session, but in the database. In this case store the credit in the database and the logged_in_user_id in the session.
3.6. Session Fixation
Apart from stealing a user's session ID, the attacker may fix a session ID known to them. This is called session fixation.

This attack focuses on fixing a user's session ID known to the attacker, and forcing the user's browser into using this ID. It is therefore not necessary for the attacker to steal the session ID afterwards. Here is how this attack works:
- The attacker creates a valid session ID: They load the login page of the web application where they want to fix the session, and take the session ID in the cookie from the response (see numbers 1 and 2 in the image).
- They maintain the session by accessing the web application periodically in order to keep an expiring session alive.
- The attacker forces the user's browser into using this session ID (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example:
<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>. Read more about XSS and injection later on. - The attacker lures the victim to the infected page with the JavaScript code. By viewing the page, the victim's browser will change the session ID to the trap session ID.
- As the new trap session is unused, the web application will require the user to authenticate.
- From now on, the victim and the attacker will co-use the web application with the same session: The session became valid and the victim didn't notice the attack.
3.7. Session Fixation - Countermeasures
One line of code will protect you from session fixation.
The most effective countermeasure is to issue a new session identifier and declare the old one invalid after a successful login. That way, an attacker cannot use the fixed session identifier. This is a good countermeasure against session hijacking, as well. Here is how to create a new session in Rails:
reset_session
If you use the popular Devise gem for user management, it will automatically expire sessions on sign in and sign out for you. If you roll your own, remember to expire the session after your sign in action (when the session is created). This will remove values from the session, therefore you will have to transfer them to the new session.
Another countermeasure is to save user-specific properties in the session, verify them every time a request comes in, and deny access, if the information does not match. Such properties could be the remote IP address or the user agent (the web browser name), though the latter is less user-specific. When saving the IP address, you have to bear in mind that there are Internet service providers or large organizations that put their users behind proxies. These might change over the course of a session, so these users will not be able to use your application, or only in a limited way.