How to hack a Rails app using its secret_token
22 Jul 2013
Create a new Rails app, open /config/initializers/secret_token.rb
and you’ll see your app’s secret_token
. As I will show you, if anyone who wishes you harm gets hold of this string then they can execute arbitrary code on your server. Troublingly, the Rails default includes it in your version control, and if you don’t remove it then anyone who gets or is given access to your codebase has complete, complete control over your server. Maybe you added them to your private repo for a code review, or unthinkingly put a side-project production app into a public repo, or someone sneaked a look at your Sublime while you were out. It doesn’t matter - if they have this key then they own you.
Why your secret_token
is important - session cookies
Your secret_token
is used for verifying the integrity of your app’s session cookies. A session cookie will look something like:
The cookie value (the part after the =
) is split into 2 parts, separated by --
. The first part is a Base64 encoded serialization of the hash that Rails will use as the session
variable in controllers. The second part is a signature created using secret_token
, that Rails uses to check that the cookie it has been passed is legit. This prevents users from forging nefarious cookies and from tricking Rails into loading data it doesn’t want to load. Unless of course they have your secret_token
and can also forge the signature…
The serialized session hash
The first part of the cookie is a Marshal dump of the session hash, encoded in Base64. Marshal is a Ruby object serialization format that is used here to allow Rails to persist objects between requests made in the same session. In many cases it will only store the session_id
, _csrf_token
and Warden authentication data, but calling session["foo"] = "bar"
in your controllers allows you to store pretty much anything you want. For my cookie above, unescaping the URL encoding and then Base64 decoding gives:
which if you squint hard enough is indeed starting to look kind of like a hash. This cookie is passed up to the server with each request, Rails calls Marshal.load
on it, and merrily populates session
with whatever serialized objects it is passed. Object persistance between requests. Brilliant.
The signature
But wait, the cookie obviously lives on the client side, which means that a user can set it to be anything they want. Which means that the user can pass in whatever serialized object they want to our app. And by the time we reinflate it and realise that they have passed us a small thermonuclear device, it will be too late and the attacker will be able to execute arbitrary code on our server.
That’s where our secret_token
and the second part of the cookie value (the part after the --
) come in. Whenever Rails gets a session cookie, it checks that it hasn’t been tampered with by verifying that the HMAC digest of the first part of the cookie with its secret_token
matches the second, signature part. This means in order to craft a nefarious cookie an attacker would need to know the app’s secret_token
. Unfortunately, just being called secret_token
doesn’t make it secret, and, as already discussed, if you aren’t careful then it can easily end up somewhere you don’t want it to.
The attack - how to bake a poisonous cookie
If you know an app’s secret_token
and want to forge a valid cookie, you simply need to reverse the above process:
This request will load my_evil_session_hash
into session
, which is purely on principal not good. But loading arbitrary strings and integers is not about to melt any servers. How can you choose the contents of your hash so as to actually do some damage? Some obscure objects buried deep inside Rails are happy to oblige:
And presto.
Knowing is half the battle
ALL of this trouble can be trivially avoided by taking secret_token
out of your version control. Put it into an environment variable (dotenv is handy for local, it’s easy on Heroku too) and you can sleep (a bit more) soundly at night. If you suspect that someone you wouldn’t want to meet on a dark night knows your secret_token
then you can simply change it. All your existing cookies will be invalidated, but nothing else bad will happen. Of course, you still don’t want anyone you don’t trust to get any kind of access to your codebase at all. But you can at least make life difficult for them even if they do.
Thanks to
Create a new Rails app, open /config/initializers/secret_token.rb
and you’ll see your app’s secret_token
. As I will show you, if anyone who wishes you harm gets hold of this string then they can execute arbitrary code on your server. Troublingly, the Rails default includes it in your version control, and if you don’t remove it then anyone who gets or is given access to your codebase has complete, complete control over your server. Maybe you added them to your private repo for a code review, or unthinkingly put a side-project production app into a public repo, or someone sneaked a look at your Sublime while you were out. It doesn’t matter - if they have this key then they own you.
Why your secret_token
is important - session cookies
Your secret_token
is used for verifying the integrity of your app’s session cookies. A session cookie will look something like:
The cookie value (the part after the =
) is split into 2 parts, separated by --
. The first part is a Base64 encoded serialization of the hash that Rails will use as the session
variable in controllers. The second part is a signature created using secret_token
, that Rails uses to check that the cookie it has been passed is legit. This prevents users from forging nefarious cookies and from tricking Rails into loading data it doesn’t want to load. Unless of course they have your secret_token
and can also forge the signature…
The serialized session hash
The first part of the cookie is a Marshal dump of the session hash, encoded in Base64. Marshal is a Ruby object serialization format that is used here to allow Rails to persist objects between requests made in the same session. In many cases it will only store the session_id
, _csrf_token
and Warden authentication data, but calling session["foo"] = "bar"
in your controllers allows you to store pretty much anything you want. For my cookie above, unescaping the URL encoding and then Base64 decoding gives:
which if you squint hard enough is indeed starting to look kind of like a hash. This cookie is passed up to the server with each request, Rails calls Marshal.load
on it, and merrily populates session
with whatever serialized objects it is passed. Object persistance between requests. Brilliant.
The signature
But wait, the cookie obviously lives on the client side, which means that a user can set it to be anything they want. Which means that the user can pass in whatever serialized object they want to our app. And by the time we reinflate it and realise that they have passed us a small thermonuclear device, it will be too late and the attacker will be able to execute arbitrary code on our server.
That’s where our secret_token
and the second part of the cookie value (the part after the --
) come in. Whenever Rails gets a session cookie, it checks that it hasn’t been tampered with by verifying that the HMAC digest of the first part of the cookie with its secret_token
matches the second, signature part. This means in order to craft a nefarious cookie an attacker would need to know the app’s secret_token
. Unfortunately, just being called secret_token
doesn’t make it secret, and, as already discussed, if you aren’t careful then it can easily end up somewhere you don’t want it to.
The attack - how to bake a poisonous cookie
If you know an app’s secret_token
and want to forge a valid cookie, you simply need to reverse the above process:
This request will load my_evil_session_hash
into session
, which is purely on principal not good. But loading arbitrary strings and integers is not about to melt any servers. How can you choose the contents of your hash so as to actually do some damage? Some obscure objects buried deep inside Rails are happy to oblige:
And presto.
Knowing is half the battle
ALL of this trouble can be trivially avoided by taking secret_token
out of your version control. Put it into an environment variable (dotenv is handy for local, it’s easy on Heroku too) and you can sleep (a bit more) soundly at night. If you suspect that someone you wouldn’t want to meet on a dark night knows your secret_token
then you can simply change it. All your existing cookies will be invalidated, but nothing else bad will happen. Of course, you still don’t want anyone you don’t trust to get any kind of access to your codebase at all. But you can at least make life difficult for them even if they do.