If you're like me, you got into this business because you love building awesome apps. If you've been in the development space long enough, you'll eventually have to do work on those awesome apps that doesn't feel so awesome. Security can be one of those things. Taking Rails security seriously is important, even though the Rails framework does much of the heavy lifting.
Before we get too deep into the details of Ruby on Rails security, let's take a second to reflect on the good times.
... and now back to the real world.
It sucks that you even have to worry about security threats. It's just sick that there are people who wake up every morning and try to smash the things you're building.
But these people exist. They work hard to poke holes in your security. And when they succeed, it's your fault.
Your client can't make your code secure on your behalf. Your manager can't do it for you. It's all on you. So you better know what the hell you're doing.
Why should you care about Rails security?
- Do you try to follow best practices for security but don't really understand them?
- Are you unclear about how XSRF, MITM, and XSR attacks work?
- Do you think that because your apps are small or internal, they're not at risk?
If any of these are true, then this guide is definitely for you. Security is important for every app, and avoiding investing in it is only fine until it isn't. Rails makes mitigating some of the biggest security risks of web applications super straightforward, so you'd be silly not to close the gap.
Do you think your application isn't a target?
You're probably wrong. If it's on the internet, it's under siege. Automated systems are scanning for vulnerable web apps around the clock.
Whether organized crime or bots, the internet is full of bad actors who want to get their hands on your users' data.
Organized crime is a real
Are you storing credit cards in your database? Unsalted password hashes? I hope not! If so, you're screwed. Rails makes avoiding mistakes like this relatively painless.
Botnets will take you down
When you run a botnet, more bots equals more money. So you buy an exploit pack, set it up to install your malware when an old browser hits the payload, and then hack some legit sites to deliver the payload to lots of users.
Foreign governments are out to get you
It sounds like a joke, but the Chinese government has thousands of people hacking systems in search of trade secrets, government intelligence, and even information about political enemies.
Doesn't that sound like a super fun job?
China isn't the only nation-state directing hacking resources around the globe. Governments around the world participate, so don't think your application isn't important enough to take security concerns seriously.
Hacktivists are lurking in the shadows
Do you sell depleted uranium slugs to the Department of Defense? Then, you should have enough money to hire someone who knows what they're doing. Hacktivists regularly target websites that align with their targets of activism.
Required reading
This is the inadequate guide. Did you expect it to be comprehensive? At minimum you should also read:
Eight Ruby security vulnerabilities (or eight easy ways to get p0wn'd)
Now that we're on the same page about how serious security is (even for small apps), you're probably wondering how to ensure your Ruby app is secure.
We're going to cover eight classes of vulnerabilities that are crucial for web developers to understand.
- Non-technical
- Cross-Site Scripting (XSS)
- Cross-Site Request Forgery (CSRF, XSRF)
- Man in the Middle (MITM)
- SQL Injection (SQLI)
- Mass Assignment & Parameter Injection
- Denial of Service (DOS, DDOS)
- Platform Attacks (Exploiting the host that runs your application)
Non-technical security exploits that can affect Ruby on Rails security
Sometimes, the obvious problems are the hardest to see. You might not see these mistakes on a checklist, but it's worth using some common sense.
The number and variety of stupid things you can do are endless, so we'll just point out a few.
Don't:
- Send plaintext passwords via email
- Use the same password everywhere
- Trust people who show up at your office with cookies
- Carry sensitive data on your laptop
Cross-site scripting
There's one type of vulnerability that lets a malicious actor take advantage of your users as they use your application.
If I'm a bad actor, I might be quite interested in ways to insert some of my JavaScript into your page.
Once I do that, I can steal active session cookies, bootstrap an exploit kit into the DOM, or even make the pages say wacky things. That may not sound terribly harmful, but even injecting malicious content can cause reputational damage.
There's a simple way to defend against cross-site scripting
To thwart attackers looking to take advantage of cross-site scripting, you need to escape any text from user input, so JavaScript gets turned into harmless text.
You want this.
<script>alert('I am stealing your cookies! Ha!');</script>
You do not want this:
<script>alert('I am stealing your cookies! Ha!');</script>
Sometimes it's not so easy to defend against cross-site scripting
Here's the thing. If you have an old Rails application running without security patches, you probably have XSS vulnerabilities. Back in the very early version of Rails, you had to manually escape untrusted input using the h()
method.
Modern Rails apps escape HTML output by default, which prevents most XSS vulnerabilities out of the box. But it’s still surprisingly easy to slip up, especially when dealing with legacy code, third-party libraries, or dynamic HTML rendering.
Even in modern versions of Rails, you can shoot yourself in the foot. Calling html_safe
on user input will bypass escaping entirely, so if you're interpolating user input into HTML and marking it safe, you're basically turning off Rails' built-in protection and inviting XSS into your application.
The Rails team acknowledges cross-site scripting as a particularly prevalent and damaging exploit, so they dedicated a section of the security documentation to ensuring we have what we need to mitigate this risk. It's worth a read, even if you think your application is safe.
Cross-site request forgery
Another common security vulnerability in Rails applications is called cross-site request forgery. Let's look at an example.
If a user wants to change their password, they post a form to the backend, right? So, what happens if the form isn't on your website?
The request still goes through. Attackers can take advantage of this fact with a careful exploit.
The anatomy of a CSRF attack
Let's say that I'm an attacker interested in exploiting your application via CSRF. If I happen to know that you're logged in to your app as an admin, I might create a "change password" form disguised as something else. You would submit the form without realizing it and change your password.
And it's even worse if your app is using GET requests instead of POST requests.
I wouldn't even have to trick you. I could cause an authenticated request to happen just by adding an image tag to a page you recently visited.
Rails saves your bacon (without too much extra effort!)
Luckily, Ruby on Rails sidesteps this class of attack by offering mostly built-in CSRF protection. You may know it as "that weird text that gets put in all my forms for some reason."
<input type="hidden" name="authenticity_token" value="kM3jN8vBx2QzE-7wRtFpL6aYsI9uZcXmD4oHgK1rVnWe5qA0bUiJlSfTyP3hG2oC8xZ7mBvNkL4wQrEtYuI6pA" autocomplete="off" />
The indecipherable text is a secret key that is stored in the user's session data. If the user submits a CSRF token that doesn't match the one in the session, your app throws an exception. Fortunately, Honeybadger can help you find and fix these exceptions!
But you can still screw it up
Of course, you can disable CSRF checking with a seemingly innocent line of code in a controller:
skip_before_action :verify_authenticity_token
You could also cause yourself problems by using GET routes for things that should be POST routes:
MyApp::Application.routes.draw do
match "/launch_all_the_missiles", to: "missiles#launch_all"
end
But you wouldn't do that, would you? Still, the scary part about CSRF is that it doesn't have to be your app that contains the problem. A user could visit another site with a CSRF exploit embedded, triggering a request to your app. Leaning on Rails' countermeasures can help you protect against this sort of thing.
Man-in-the-middle attacks and packet sniffing
Man-in-the-middle attacks are best explained with an analogy. Let's say you need to send some cash to your rotten, no-good cousin Vinny. You might take the cash, put it in an envelope, write that bastard's address, add a stamp, and hand the envelope over to the mail carrier.
A week later, the envelope arrives to Vinny's home with no cash inside. What happened? Someone tampered with the contents of the envelope between you and the intended recipient. This is a simple man-in-the-middle attack.
Application cookies are vulnerable to man-in-the-middle attacks
The story above might sound a little silly, but the same thing can happen with your users' cookies.
Cookies are usually sent with every web request. If they're sent in cleartext over a public (wifi) network, it's game over.
An evil-doer running something like Firesheep can easily get copies of the user's cookies and compromise their account.
SSL to the rescue
The easy answer is to use HTTPS for everything and follow strict transport security. The people in the cafe can still see the user's traffic, but it's encrypted. A not-so-easy-but-perhaps-better-in-some-circumstances answer is to use Rails' secure cookies.
Secure cookies will only be sent to the browser over an HTTPS connection, which has been the default since Rails 3.
If you're unlucky enough to be running a version of Rails older than that, secure cookies could be sent over plain text. Not so secure, huh?
SQL Injection
Most of the websites on the internet work by gluing fragments of text together to make little programs in a language called SQL.
Just think about that. Do you have any idea how crazy this is?
But, like it or not, this is the way things work. And based on the fact that the internet exists, it must work reasonably well.
One side effect of slinging SQL around on the internet is that if a hacker can add their own bits of text into little SQL programs, your data could be exposed to problems.
An example of SQL injection
Imagine that your contractor sends you some code that looks like this.
Building.where("st_intersects(st_setsrid(st_makebox2d(st_point(#{params[:northeast][:lng]}, #{params[:northeast][:lat]}), st_point(#{params[:southwest][:lng]}, %{params[:southwest][:lat]})), 4326), buildings.location)")
This is a huge mistake that could open you up to a SQL injection. Accessing the params directly in an SQL query gives the client the option to force their own SQL to execute. An astute attacker can make the params[:southwest][:lat]
contain some malicious SQL code like this:
")), 4326), buildings.location); UPDATE users SET admin=1 WHERE id=666;--"
And now my user ID has admin privileges. SQL injections are common and easy exploits, so anyone who watches their site's traffic long enough will eventually see some attempts to SQL inject.
Rails protects us from SQL injection almost automatically
Rails protects from SQL injection in many cases, as long as you let it. If we'd used where
correctly, the parameters would have been escaped, and SQL injection attempts would have failed.
where("st_intersects(st_setsrid(st_makebox2d(st_point(?, ?), st_point(?, ?)), 4326), #{table_name}.location)", box[:northeast][:lng], box[:northeast][:lat], box[:southwest][:lng], box[:southwest][:lat]).
Rails doesn't always protect against SQL injection
But there are a lot of places where Rails uses raw SQL. Places you might easily overlook.
For example, did you know that in SomeModel.sum("amount")
, the "amount" string is added to the query without being escaped?
So if you have a report where you call the sum
method with a direct access of params["col"]
, you're in trouble.
Here's an example IRB session, showing the consequences of passing untrusted input into the sum method:
pry(main)> User.sum(%[id) AS sum_id FROM users; update users set
admin='t' where id=224;--])
(22.4ms) SELECT SUM(id) AS sum_id FROM users; update users set
admin='t' where id=224;--) AS sum_id FROM "users"
=> "0"
The same is true for #pluck
, as well as a host of other methods.
Mass assignment attacks
March 4, 2012, Russian hacker Egor Homakov disclosed a mass assignment vulnerability that could let an attacker masquerade as any GitHub user. As you can imagine, this was extraordinarily problematic.
How GitHub was hacked
I bet you've written code like this hundreds of times:
User.create(params[:user])
Thanks to the readability of Ruby code, it's pretty easy to tell that this code creates a user with the given parameters.
Prior to Rails 3.2.3, that meant that any parameter that was input would be assigned to the model unless you explicitly specified otherwise.
The problem this introduced was that it was easy to forget. And so you find yourself in a world where an attacker just has to send in the right POST request to cause this to happen:
User.create({admin: true, ...})
Or in GitHub's case, something like this:
PublicKey.update({user_id: "123"})
Mass assignment is an easy fix
The preferred solution to mass assignment vulnerabilities in modern Rails apps is to use Strong Parameters, which you should be familiar with from earlier in this article.
Instead of blindly passing the entire params
hash to User.create
, you explicitly declare which attributes are allowed:
def user_params
params.require(:user).permit(:name, :email)
end
User.create(user_params)
This simple choice protects your models by allowlisting just the fields you intend to update. It’s the default protection in Rails 4 and beyond. If you're maintaining a really old app (Like Rails 4 or older), it's worth upgrading.
Denial of service attacks
How many people have you pissed off today? What about last month?
Denial of service attacks (DOS) and distributed denial of service attacks (DDOS) are the infosec community's version of a rage comic. These attacks are not going to steal your data or spread malware. They just want to shut you down. This can still cause massive financial or reputational damage to your company.
DDOS is a crude but effective attack
To mount a distributed denial of service attack, just call up 500 of your closest friends, have them head over to Amnesty International's website, and keep pressing refresh for an hour. Bots can be substituted for friends
If you have problems like these, there are a number of solutions. Rails isn't super focused on this sort of attack, but there are a number of gems that can help you quickly introduce rate limiting. Rails also has throttling middleware you can use. You may also consider talking to your ISP and using a proxy like Cloudflare, which offers robust rate-limiting features. These are pretty boring but well-known problems.
Algorithmic complexity attacks
That just sounds freaking cool, doesn't it?
These attacks take advantage of a particular weakness in your operating system, application stack, and so on, causing problems on the application host. They can be used to fill up your server's memory, increase your CPU utilization, or even fill up disk space.
For example, the classic SYN flood attack works by sending SYN requests to a server. The server opens a ton of connections which aren't closed. With enough open connections, the operating system will quickly run out of memory, and the application itself will crash. This exploits an algorithmic reality to cause a meaningful problem that can take down an application altogether.
Good old-fashioned server exploits
This is really what we think of when we think of hackers, right? Shady people using buffer overflows to cause Apache to run arbitrary code as the root user?
But like many boring jobs, this, too, has been automated. If you look at your system logs right now, I bet you'll see bots trying to brute-force an SSH login.
Automate attack detection
You can use a popular tool like fail2ban to automatically block the IP addresses of hosts that are attempting known attacks against you.
Do the things you know you should do.
Keep your operating system patched. Don't run services you don't need. Don't allow password authentication for SSH. The list goes on. If you use a platform, much of this is handled for you.
It's time to level up your Rails security
First, don't panic! Rails security is part of your job whether you like it or not. Now that you're taking it seriously, you'll make quick progress in mitigating vulnerabilities. An underrated but effective win is to make sure you're using a version of Rails that is still receiving security updates. All the SQL escaping in the world can't protect you if the framework is running on a version that has currently published critical vulnerabilities. The same goes for your gem dependencies.
Once the framework is supported, use this guide (and the Rails security guide!) to reduce your app's surface area of risk. Use strong parameters. Configure a rate limiter and maybe a proxy. Follow HTTP conventions for methods and force traffic to be SSL. All these things will reduce the likelihood that you will suffer a security problem bit by bit.