Sharing Rails Sessions With Non Ruby Apps
I wanted to share sessions between my Rails and Go applications. I wanted to let an authenticated Rails user make JavaScript API calls to an endpoint written in Go. How hard could it be?
Since I own both apps, I thought it would be as simple as sharing the secret session key and re-implementing Rails crypto process in Go. It turned out to be a lot more interesting.
In a nutshell, here is what I discovered:
- It’s totally doable! Here is my Go package.
- If you are using a version of Rails older than 4.0, you’d better upgrade ASAP!
- Rails has been criticized for security issues, but the current solution has been vetted by many experts.
- Rails serializes session data using Ruby Marshal which means that someone with the secret key can inject arbitrary code in the session and it will execute server side. Switch to JSON, MessagePack or other safe serialization formats.
- Security is (still) hard.
Rails Cookies are Dangerous
Because Rails serializes and deserializes the session and any encrypted/signed cookies using Ruby’s Marshal library, someone with the app secret can wreak havoc. They can embed arbitrary Ruby code into the cookie, submit it with a request, and the server-side deserialization will execute that code without you noticing. Granted, this requires the attacker to have the app secret, but since 99% of the apps out there have the shared secret in their source code, anyone with access to the source code has this data. It’s not data you can easily rotate when employees leave or when you are done working with contractors. Anybody with the shared secret is a potential attacker. Start by moving this data out of the code base and into an environment variable.
Rails doesn’t let you change the default serializer directly. But Rails relies on ActiveSupport for its crypto work and AS supports swapping the serializer. Some people in the community are aware of this issue and monkey patch Rails to serialize their sessions using JSON or another alternative. Here is an Airbnb article and Rails 3 patch. Here is my Rails 4 monkey patch to switch the serialization to JSON. I’m using it in production with Rails 4, but it’s untested on Rails 3.
You can modify either solution to use MessagePack instead of JSON if you want to fit more data in the 4K cookie size.
Understanding Rails Session Encryption
Once I addressed the serialization issue, I had to reimplement the crypto work done by Rails to encode and/or sign the data.
Most of us just rely on our frameworks/libraries to do the right thing, but we rarely look under the hood. I ported the logic to Golang which has an amazing support for crypto (albeit lower level than Ruby). My Go package contains an explanation of the code logic and the examples needed to decode/verify as well as encode/sign sessions that are compatible with Rails.
Here is a high level summary of what Rails does when it encodes and signs your session data:
key_generator = ActiveSupport::CachingKeyGenerator.new(ActiveSupport::KeyGenerator.new(app_secret_key, iterations: 1000))
derived_secret = key_generator.generate_key("encrypted cookie")
sign_secret = key_generator.generate_key("signed encrypted cookie")
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
session_content = encryptor.encrypt_and_sign({hello: "world"})
The session_content
string is then set as the session cookie value.
Note that you could do that in any Ruby app using ActiveSupport
, making it easy to share sessions between Ruby applications (like Rails & Sinatra).
Technically, there are a lot of things going on. To avoid using the same secret to sign and encode data, Rails relies on derived keys using PBKDF2 (password based key derivation function). It treats the app secret as a password and applies a pseudorandom function 1000 times (Rails default) using a default salt. The result is a derived key so the original password isn’t shared. The derived key can be regenerated identically if the salt and secret are known (because the function is pseudorandom).
The two derived keys are then passed to the MessageEncryptor
class which uses MessageVerifier
to do the signing. The generated keys are 64 bytes long. One key goes to the encryptor while the other goes to the verifier.
The verification is done via HMAC (SHA1) and it uses the full 64 byte key. The encoding is done via AES 256 CBC only using the first 32 bytes of the encryption derived key. Rails will only generate a 32-byte key since that’s the expected key length.
The session data is serialized (using Marshal by default) then encoded via AES. Both the encoded string and the IV are encoded using base64 and joined in a string using a predefined format.
At this point, the session is encoded but it could be tampered with. To avoid that, Rails signs the encoded data using the verifier (HMAC) and appends the base 64 encoded signature to the encoded data.
To decode and verify the data, Rails repeats the process in reverse using the serializer to deserialize the data.
Note that you can also rely on the the same crypto process to safely encode/sign any data you want to share. If you’re ok with the data being user-readable, sign it to make sure it isn’t tampered with along the way. If you don’t want it to be user-readable, encrypt it first then sign the encrypted data.
Sharing the Session with Non-Ruby Apps
Many apps are moving to an SOA approach. That often means multiple languages living together in production. Sharing a web session can be very useful, especially until you switch to a SSO solution.
The key is to start by having the session data serialized in a format that is available in all your relevent languages. JSON, XML MessagePack, and protobuf are good examples.
The second step is to reimplement the crypto dance I just explained above. The good news is that I’ve already done it for Go. Using that example, you should be able to port it to other languages (Node, Scala/Clojure/Java, Rust, Elixir, Python or whatever you fancy).
https://github.com/mattetti/goRailsYourself/tree/master/crypto
Even though the test suite isn’t perfect (yet), it should greatly help you through the porting process. To be honest the hardest part was understanding the process, not writing the code. Most languages have decent crypto libraries to do the hard parts for you. But for Go I had to implement lower level pieces like the PKCS7 padding for the AES CBC encryption/decryption.
Hopefully this article was helpful and you now better understand how Rails does its session encryption. Once you understand the process Rails uses, you can implement it in any language.
** Finally, if you interested in working on interesting and challenging problems like these ones, consider joining the Splice team! **