Some additional attacks on Cookie Session

Aside from the replay attacks discussed, there are some other attack
vectors on the cookie_session store.

I appreciate (and admire!) Jeremy's good humor on all of this:

Planting the seed here led to quick ripening and plenty of pesticide.
Thanks for the fish, all.

jeremy

Anyway, here's what we came up with:

1. Brute Force
SHA512 can be computed _very_ fast. On my Pentium Core Duo:

z = 'z' * 100; puts Benchmark.measure { 1000.times

{ Digest::SHA512.hexdigest(z) }}
  0.032000 0.000000 0.032000 ( 0.031000)

That's 0.03 ms/hash using simple Ruby code, not optimized C /
Assembly!

That means a simple brute force attack can go through the entire
dictionary in a few seconds. Even using multiple word phrases, and
inserting several digits, we can complete the attack in reasonable
time. We can even search brute force letter combinations, esp. if we
only generate pronouncable phoneme combinations.

2. Entropy
Related to #1: to resist brute force attacks, you really want 128
bits, and preferably 256 bits, of entropy. The source code suggests
"some secret phrase", which is unlikely to come even close. The way
to create a key is to use a PRNG seeded with true, system level
entropy.

3. Rainbow Tables
Since there will be standard data common among many apps (eg null
sessions), and no salting is employed, we can use create Rainbow
Tables, and afterwards find the secret very quickly.

4. Precomputation
Cookie session computes the hash by *appending* the secret to the
data. This can be used to speed up the brute force, by precomputing
the hash of the data, and starting the hash function on the candidate
session. The correct way to use the key is to repeatedly XOR it to
the data, not append it.

5. Key Storage
One of the most common crypto truisms developers know is "Don't store
passwords in the clear". Backups are made, files are transferred, and
the bad guys job is made too easy. In cookie_store, the app's key is
stored directly in the clear. No ephemeral key is used. If an
attacker see's this, it's equivalent to getting *everyone's*
password. Even worse, since he can create arbitrary sessions.

To make matters worse, the source code seems to suggest storing the
key in the app's source code itself. So, every developer, every
subversion check out, every code base back up, has the key.

6. Key Expiry
There's no way to expire the current key without destroying all of the
current sessions. Keys are intended to be (relatively) permanent and
require manual actions to change. This makes all of the above attacks
both easier and more dangerous.

7. Key / Session Revocation
Likewise, should you suspect key compromise - to revoke the key, you
need to destroy all of the current sessions. And there's also no way
to revoke an individual session, should you get a call "uh, I logged
in at the library yesterday and now someone's reading my mail".

Some of the above can be corrected easily. Some are much more
challenging. I think they all should demonstrate that creating a
crypto system is quite formidable.

Below is a simple proof of concept code to demonstrate #1. It's
simple Ruby: an optimized native version could be expected to be 100
times faster.

# cookie_crumbler.rb

include 'base64'
include 'digest/sha2'

cookie = ARGV[0]
data, digest = cookie.split('--')

# You can replace this with any object supporting #each,
# such as a brute force generator
wordlist = File.open('/usr/share/dict/words')

wordlist.each do |line|
  secret = line.chomp
  if Digest::SHA512.hexdigest(data + secret) == digest
    puts "Secret found: #{secret.inspect}"
  end
end

Great work, and an excellent analysis. Comments interspersed.

Anyway, here's what we came up with:

1. Brute Force
SHA512 can be computed _very_ fast. On my Pentium Core Duo:
> z = 'z' * 100; puts Benchmark.measure { 1000.times
{ Digest::SHA512.hexdigest(z) }}
  0.032000 0.000000 0.032000 ( 0.031000)

That's 0.03 ms/hash using simple Ruby code, not optimized C /
Assembly!

That means a simple brute force attack can go through the entire
dictionary in a few seconds. Even using multiple word phrases, and
inserting several digits, we can complete the attack in reasonable
time. We can even search brute force letter combinations, esp. if we
only generate pronouncable phoneme combinations.

2. Entropy
Related to #1: to resist brute force attacks, you really want 128
bits, and preferably 256 bits, of entropy. The source code suggests
"some secret phrase", which is unlikely to come even close. The way
to create a key is to use a PRNG seeded with true, system level
entropy.

Agreed that "some secret phrase" will not yield 256 or even 128 bits of entropy. But the Rails app generator uses a version of generate_unique_id, which uses just about all of the system-level entropy available to Ruby. Granted, it's an MD5 hash (thus an upper limit of 128 bits of entropy), not a cryptographic PRNG, but it is better than a user-entered phrase by far.

3. Rainbow Tables
Since there will be standard data common among many apps (eg null
sessions), and no salting is employed, we can use create Rainbow
Tables, and afterwards find the secret very quickly.

The developer must set a session cookie name, and all of the examples given so far use "_appname_session". So there is a security advantage to choosing your own session key name and keeping it secret (otherwise an attacker can create a rainbow table specific to your application).

4. Precomputation
Cookie session computes the hash by *appending* the secret to the
data. This can be used to speed up the brute force, by precomputing
the hash of the data, and starting the hash function on the candidate
session. The correct way to use the key is to repeatedly XOR it to
the data, not append it.

Excellent point.

5. Key Storage
One of the most common crypto truisms developers know is "Don't store
passwords in the clear". Backups are made, files are transferred, and
the bad guys job is made too easy. In cookie_store, the app's key is
stored directly in the clear. No ephemeral key is used. If an
attacker see's this, it's equivalent to getting *everyone's*
password. Even worse, since he can create arbitrary sessions.

To make matters worse, the source code seems to suggest storing the
key in the app's source code itself. So, every developer, every
subversion check out, every code base back up, has the key.

I would assume they'd throw a YAML.load from a config file in there before pushing this to stable. But you're right, this is considerably different than a database password.

I'm not sure how an ephemeral key would work given that sessions may stick around arbitrarily long... were you suggesting that there is a solution, or were you only pointing out the problem?

6. Key Expiry
There's no way to expire the current key without destroying all of the
current sessions. Keys are intended to be (relatively) permanent and
require manual actions to change. This makes all of the above attacks
both easier and more dangerous.

7. Key / Session Revocation
Likewise, should you suspect key compromise - to revoke the key, you
need to destroy all of the current sessions. And there's also no way
to revoke an individual session, should you get a call "uh, I logged
in at the library yesterday and now someone's reading my mail".

Some of the above can be corrected easily. Some are much more
challenging. I think they all should demonstrate that creating a
crypto system is quite formidable.

This has been very enlightening. Thanks for taking the time to write this up.

--be

> 2. Entropy
> Related to #1: to resist brute force attacks, you really want 128
> bits, and preferably 256 bits, of entropy. The source code suggests
> "some secret phrase", which is unlikely to come even close. The way
> to create a key is to use a PRNG seeded with true, system level
> entropy.

Agreed that "some secret phrase" will not yield 256 or even 128 bits
of entropy. But the Rails app generator uses a version of
generate_unique_id, which uses just about all of the system-level
entropy available to Ruby. Granted, it's an MD5 hash (thus an upper
limit of 128 bits of entropy), not a cryptographic PRNG, but it is
better than a user-entered phrase by far.

Missed that - which file is this in?

I'm not sure how an ephemeral key would work given that sessions may
stick around arbitrarily long... were you suggesting that there is a
solution, or were you only pointing out the problem?

Sessions would need to be refreshed - automatically by any request -
or would indeed expire.

This has been very enlightening. Thanks for taking the time to write
this up.

You're most welcome.

It's buried:

railties/lib/rails_generator/generators/applications/app/app_generator.rb

Here's the relevant code:

# duplicate CGI::Session#generate_unique_id
md5 = Digest::MD5.new
now = Time.now
md5 << now.to_s
md5 << String(now.usec)
md5 << String(rand(0))
md5 << String($$)
md5 << @app_name

# ...

m.template "environments/environment.rb", "config/environment.rb", :assigns => { :freeze => options[:freeze], :app_name => @app_name, :app_secret => md5.hexdigest }

--be

AFAIR the session ID is stored in a cookie for all the other session
stores. Doesn't much of what you say apply to the entire Rails session
system?

No. The examples he gives are a specific consequence of using a cryptographic hash to sign the session, and storing it on an untrusted client. It has nothing to do with the fact that the sessions are stored in a cookie, and everything to do with the fact that without additional shared state, the only information the server has to go on is what is in the cookie.

This is all working under the assumption that you could determine the
salt in a reasonable amount of time.

-PJ
http://errtheblog.com

The cookie session store does not use a salt.