Gastón Ramos
Personal blog
Ruby on Rails Security
Originally written in four Pomodoros on the morning of July 4, 2024.
Leer el artículo original en español
I want to write a little about security in Ruby on Rails. Web application security is an enormous subject and can easily lead to confusion. As programmers, we might say the million-dollar question is: how can we ensure that our application is secure? In fact, can we ever guarantee that an application is secure?
I don't think that question has a simple answer. Security is such a broad field that it is very difficult to say, “I guarantee this application is one hundred percent secure.” I have about twenty years of experience developing web applications and APIs, and I still feel that there is a great deal I don't know about security. So let me reframe the question: what can we, as Ruby on Rails programmers, do to reduce security problems as much as possible?
Two stages in an application's life cycle come to mind. The first is when we are building an application from scratch or developing a new feature. While doing that work, we need to keep several security concerns in mind. A good place to start learning about them is the Ruby on Rails Security Guide.
The guide contains a great deal of information about common vulnerabilities in Rails and web applications. If I had to reduce the list to five, my top five would be:
- Session hijacking
- Cross-Site Request Forgery (CSRF)
- SQL injection
- Command injection
- Cross-Site Scripting (XSS)
Session hijacking
Many web applications have an authentication system. A user provides a username and password, and the application stores the user's ID in a session cookie. From that point on, requests to the application's server don't require the user to enter their credentials again because the session identifies them. The cookie therefore acts as temporary authentication for the web application. Anyone who obtains another person's session cookie may be able to use the application as that person.
Cross-Site Request Forgery (CSRF)
In a CSRF attack, an attacker tricks the user's browser into sending a malicious HTTP request to a site where the user is already authenticated. Imagine that you're signed in to your online bank and an attacker sends you an email containing a malicious link. When you click it, your browser sends a transfer request to the bank. Because you're already authenticated, the bank may process the request without you realizing what happened.
Rails protects against this problem by using a CSRF token. This protection is enabled by default in modern versions of Rails.
config.action_controller.default_protect_from_forgery = true
It can also be enabled explicitly in a controller:
protect_from_forgery with: :exception
SQL injection
SQL injection attacks manipulate web application parameters to influence database queries. One common objective is to bypass authorization.
Project.where("name = '#{params[:name]}'")
This might be part of a search action in which the user enters the name of
a project. If a malicious user enters ' OR 1) --, the resulting
query would look like this:
SELECT * FROM projects WHERE (name = '' OR 1) --')
Instead of interpolating user input into a SQL string, use Active Record's parameterized conditions. They quote untrusted values safely:
Model.where("zip_code = ? AND quantity >= ?", entered_zip_code, entered_quantity).first
Command injection
If an application needs to run commands on the underlying operating
system, Ruby provides methods such as system, exec,
spawn, and backticks. Take special care when a user can provide
all or part of a command.
user_input = "hello; rm *"
system("/bin/echo #{user_input}")
See the problem?
One way to avoid it is to pass the command and its arguments separately:
system("/bin/echo", "hello; rm *")
This prints hello; rm * as text instead of executing the second
command.
Cross-Site Scripting (XSS)
Cross-Site Scripting is a vulnerability that allows an attacker to inject malicious scripts into pages viewed by other users. Imagine a profile page where users can post status updates. If the application doesn't escape the content correctly, an attacker might publish a status like this:
<script>alert('XSS!');</script>
When someone visits the attacker's profile, they see an alert containing “XSS!”. In a real attack, the script could steal session data and send it to the attacker's server.
One option is to use an allowlist of permitted tags with Rails'
sanitize helper:
tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(user_input, tags: tags, attributes: %w(href title))
This permits only the specified tags and handles many tricks involving
malformed markup. Another tool is html_escape, or its alias
h, which replaces HTML input characters such as ampersands,
quotes, less-than signs, and greater-than signs with escaped
representations.
Step two: maintenance in production
Earlier I mentioned two stages. The second is maintaining an application in production. We can't build something, deploy it, and forget about it. Just as vehicles need maintenance, applications do too. We need to follow newly disclosed vulnerabilities in Ruby, Ruby on Rails, the important gems we depend on, and the operating system itself. If a vulnerability is found in Ruby's standard library, for example, we may need to upgrade to a Ruby version that fixes it.
This was a brief introduction to five common web vulnerabilities. There are many others, and the Rails Security Guide is a good place to continue learning about them.
::: If you'd like to comment, email me: ramos.gaston AT gmail.com :::