Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

Sorry, you do not have permission to ask a question, You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please type your username.

Please type your E-Mail.

Please choose an appropriate title for the post.

Please choose the appropriate section so your post can be easily searched.

Please choose suitable Keywords Ex: post, video.

Browse

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Navigation

  • Home
  • About Us
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask a Question
  • Home
  • About Us
  • Contact Us
Home/ Questions/Q 8053

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Latest Questions

Author
  • 60k
Author
Asked: November 28, 20242024-11-28T09:33:12+00:00 2024-11-28T09:33:12+00:00

Demystifying cookie security in Rails 6

  • 60k

This blog post was originally published on my blog.

I've also given a talk based on this post. Check it out here!

Cookies are used in pretty much every modern web application. They're used for various purposes such as facilitating user authentication and storing user preferences. Since they're so widely used it's no surprise that a full-stack development framework like Rails has a simple and convenient API to manage them.

In this post I'll describe the different types of cookies supported by Ruby on Rails and how they work under the hood.

Types of Cookies in Rails

Rails supports the storage of 3 kinds of cookies:

  • Plain text : These cookies can be viewed and changed by a user.

  • Signed : Signed cookies look like gibberish but they can easily be decoded by a user although they can't be modified as they are cryptographically signed. 

  • Encrypted : Encrypted cookies can't be decoded by a user (not easily, anyway) and nor can they be modified as they are authenticated at the time of decryption.

Plain Text Cookies

Plain text cookies should be used very cautiously and sparingly. They can be viewed and changed to any value by a user without our application ever knowing. A good use case for a plain text cookie would be to store whether or not a welcome message has been shown to the user.

You can set such a cookie with a single line of code in a controller action:

  def show   cookies[:welcome_message_shown] = "true" end   
Enter fullscreen mode Exit fullscreen mode

This line will add a Set-Cookie HTTP header to the response; with the value welcome_message_shown=true. When the browser receives the response, it will store the cookie and send it as a header with every subsequent request. You can view the cookie under the Storage tab of your browser's developer tools.

Inspecting the value of a cookie using a browser's developer tools

The value of the cookie can be changed by double-clicking and modifying the value field. In this case it doesn't matter as the worst case is the user should be shown a welcome message again. For any sensitive information, signed or encrypted cookies should be used.

Signed Cookies

Signed cookies are designed to store information that is harmless for a user to view but not modify. Values such as a user id or the user's preferences are ideal candidates for signed cookies.

The value of a signed cookie is serialized along with some metadata before being encoded and signed. The default serializer is JSON but this can be changed in the cookies_serializer.rb file under the config/initializers directory.

Under the hood, Rails uses the ActiveSupport::MessageVerifier API to encode and sign the cookie data.

These cookies can also be read in JavaScript (as demonstrated later) so they're a great way to send user specific data from your database to your JavaScript application.

Storing a signed cookie is as easy as storing a plain text cookie:

  def show   cookies.signed[:user_id] = "42" end   
Enter fullscreen mode Exit fullscreen mode

This results in a cookie that looks like gibberish to the naked eye.

  "eyJfcmFpbHMiOnsibWVzc2FnZSI6IklqUXlJZz09IiwiZXhwIjpudWxsLCJwdXIiOiJjb29raWUudXNlcl9pZCJ9fQ%3D%3D--94afbf4575daf37313f40d6342a994a5e1719d79"   
Enter fullscreen mode Exit fullscreen mode

There's two parts to this string and they're separated by the --. The first part is a Base64 encoded JSON object containing the value we stored and the second part is a cryptographically generated digest. When Rails receives a signed cookie, it compares the value to the digest and if they don't match, the cookie's value will be nil'd. That's why a user cannot modify a signed cookie.

Decoding signed cookies

A signed cookie can be decoded with the following Ruby code:

  cookie = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IklqUXlJZz09IiwiZXhwIjpudWxsLCJwdXIiOiJjb29raWUudXNlcl9pZCJ9fQ%3D%3D--94afbf4575daf37313f40d6342a994a5e1719d79" cookie_value = cookie.split("--").first cookie_value = URI.unescape(cookie_value) cookie_payload = JSON.parse Base64.decode64(cookie_value)   
Enter fullscreen mode Exit fullscreen mode

The above code extracts the Base64 encoded JSON object by splitting the cookie value on the --. It then unescapes the value, decodes it and parses it into a Hash that looks like:

  {    "_rails"=> {     "message"=>"IjQyIg==",      "exp"=>nil,      "pur"=>"cookie.user_id"   } }   
Enter fullscreen mode Exit fullscreen mode

The only attribute that's relevant here is message. exp (expiry) and pur (purpose) are values used by ActiveSupport::MessageVerifier during decoding and validation.

The message is also a Base64 encoded JSON object so we decode it the same way as above:

  decoded_stored_value = Base64.decode64 cookie_payload["_rails"]["message"] stored_value = JSON.parse decoded_stored_value # => "42"   
Enter fullscreen mode Exit fullscreen mode

Since the message is stored as a Base64 encoded JSON object, we can store any JSON serializable object in a signed cookie; it doesn't have to be a string. However to store other kinds of objects, it needs to be placed in a Hash with the key value.

  def show   cookies.signed[:preferences] = {      value: {       use_dark_mode: true     }   } end   
Enter fullscreen mode Exit fullscreen mode

Decoding signed cookies using JavaScript

The above Ruby code to decode a signed cookie can be translated into JavaScript very easily. So if you need use information stored in signed cookies on the client, you can!

  let cookie = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IklqUXlJZz09IiwiZXhwIjpudWxsLCJwdXIiOiJjb29raWUudXNlcl9pZCJ9fQ%3D%3D--94afbf4575daf37313f40d6342a994a5e1719d79" let cookie_value = unescape(cookie.split("--")[0]) let cookie_payload = JSON.parse(atob(cookie_value))  let decoded_stored_value = atob(cookie_payload._rails.message) let stored_value = JSON.parse(decoded_stored_value)  console.log(stored_value) // => "42"   
Enter fullscreen mode Exit fullscreen mode

How the digest is computed

The second half of a signed cookie is the digest which is used to verify its validity. It's calculated using OpenSSL with the SHA1 hash function as the default. The hash function can be changed by setting config.action_dispatch.signed_cookie_digest in your application.rb.

The hash function requires a secret in addition to the data to be hashed. The secret is also calculated using OpenSSL and is based on the secret_key_base that you find in your credentials.yml file and another string called a salt. By default the salt is “signed cookie”, but it can be changed by setting config.action_dispatch.signed_cookie_salt.

Following the same methods as used in the Rails source code, we can calculate the digest with the following code:

  cookie = "eyJfcmFpbHMiOnsibWVzc2FnZSI6IklqUXlJZz09IiwiZXhwIjpudWxsLCJwdXIiOiJjb29raWUudXNlcl9pZCJ9fQ%3D%3D--94afbf4575daf37313f40d6342a994a5e1719d79" cookie_value = URI.unescape(cookie.split("--").first)  secret = Rails.application.secret_key_base key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret, "signed cookie", 1000, 64) digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get("SHA1").new, key, cookie_value) # => "94afbf4575daf37313f40d6342a994a5e1719d79"  digest == cookie.split("--").second # => true   
Enter fullscreen mode Exit fullscreen mode

As you can see, the digest calculated using OpenSSL matches the digest part of the cookie. So if an attacker tried to modify the data in the cookie, the digest would no longer match and Rails would nil the content of the cookie. The only way an attacker could calculate a valid digest is if they knew the secret_key_base and salt; which is why it's critical to keep these values safe.

In practice, Rails uses ActiveSupport::KeyGenerator and ActiveSupport::MessageVerifier to abstract away the OpenSSL functions. However I used OpenSSL directly in the demo above for clarity. Those encryption functions can be used in any programming language to encode and decode Rails cookies; so if you have services in your infrastructure that aren't written using Rails, you can still use the data in Rails cookies quite easily.

Encrypted Cookies

Any sensitive data stored in cookies should ALWAYS be encrypted. A remember_token is often used by applications to keep a user logged in even if they close the browser. This information is as sensitive as a user's password so it's a great example of the kind of thing that should be stored in an encrypted cookie.

Encrypted cookies are serialized in the same way as signed cookies and they're encrypted using ActiveSupport::MessageEncryptor (which uses OpenSSL under the hood). 

Let's create an encrypted cookie and see what it looks like:

  def show   cookies.encrypted[:remember_token] = "token" end   
Enter fullscreen mode Exit fullscreen mode

This sets a cookie that looks like:

  "aDkxgmW4kaxoXBGnjxAaBY7D47WUOveFdeai5kk2hHlYVqDo7xtzZJup5euTdH5ja5iOt37MMS4SVXQT5RteaZjvpdlA%2FLQi7IYSPZLz--2A6LCUu%2F5AsLfSez--QD%2FwiA2t8QQrKk6rrROlPQ%3D%3D"   
Enter fullscreen mode Exit fullscreen mode

As seen above, an encrypted cookie is divided into 3 parts separated by --, rather than two parts like a signed cookie. The first part is the encrypted data. The second part is called an initialization vector, which is a random input to the encryption algorithm. And the third part is an authentication tag, which is similar to the digest of a signed cookie. All three parts are Base64 encoded.

By default, cookies are encrypted with AES using a 256-bit key in Galois/Counter Mode (aes-256-gcm). This can be changed by setting config.action_dispatch.encrypted_cookie_cipher to any valid OpenSSL::Cipher algorithm.

Decrypting encrypted cookies

The cookie is encrypted with a key that's generated in the same way as the key used to calculate the digest of a signed cookie. So we'll need the application's secret_key_base to be able to decrypt the cookie. By default, the salt is “authenticated encrypted cookie” but it can be changed by setting config.action_dispatch.authenticated_encrypted_cookie_salt.

Using the Rails source code as a reference, we can decrypt the cookie as follows:

  cookie = "aDkxgmW4kaxoXBGnjxAaBY7D47WUOveFdeai5kk2hHlYVqDo7xtzZJup5euTdH5ja5iOt37MMS4SVXQT5RteaZjvpdlA%2FLQi7IYSPZLz--2A6LCUu%2F5AsLfSez--QD%2FwiA2t8QQrKk6rrROlPQ%3D%3D" cookie = URI.unescape(cookie) data, iv, auth_tag = cookie.split("--").map do |v|    Base64.strict_decode64(v) end cipher = OpenSSL::Cipher.new("aes-256-gcm")  # Compute the encryption key secret_key_base = Rails.application.secret_key_base secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, "authenticated encrypted cookie", 1000, cipher.key_len)  # Setup cipher for decryption and add inputs cipher.decrypt cipher.key = secret cipher.iv  = iv cipher.auth_tag = auth_tag cipher.auth_data = ""  # Perform decryption cookie_payload = cipher.update(data) cookie_payload << cipher.final cookie_payload = JSON.parse cookie_payload # => {"_rails"=>{"message"=>"InRva2VuIg==", "exp"=>nil, "pur"=>"cookie.remember_token"}}  # Decode Base64 encoded stored data decoded_stored_value = Base64.decode64 cookie_payload["_rails"]["message"] stored_value = JSON.parse decoded_stored_value # => "token"   
Enter fullscreen mode Exit fullscreen mode

The above code should be pretty self-explanatory in demonstrating how OpenSSL is used to decrypt a cookie. Since the secret_key_base is required to decrypt a cookie and that is a highly sensitive piece of information, it should NEVER be sent to the client and hence encrypted cookies should never be decrypted in your JavaScript application.

Lifetime of a Cookie

By default, a cookie expires with the browser's “session”. That means that when the user closes the browser, all cookies with an expiry date of Session will be deleted. 

Cookies can be made to persist between sessions by specifying an expiry date:

  def show   cookies[:welcome_message_shown] = {     value: "true",     expires: 7.days   } end   
Enter fullscreen mode Exit fullscreen mode

Rails also has a special permanent cookie type which sets the expiry date for 20 years in the future.

  def show   cookies.permanent[:welcome_message_shown] = "true" end   
Enter fullscreen mode Exit fullscreen mode

Signed and encrypted cookies can be chained with the permanent type to persist them across browser sessions.

  def show   cookies.signed.permanent[:user_id] = "42" end   
Enter fullscreen mode Exit fullscreen mode

  def show   cookies.encrypted.permanent[:remember_token] = "token" end   
Enter fullscreen mode Exit fullscreen mode

The special session cookie

Rails provides a special kind of cookie called a session cookie which, as the name suggests has an expiry of Session. This is an encrypted cookie and stores the user's data in a Hash. It's a great place to store things like authentication tokens and redirect locations. Rails stores Flash data in the session cookie.

Data can be stored in the session cookie similarly to regular cookies:

  

def create
session[:auth_token] = "token"
end

Enter fullscreen mode Exit fullscreen mode



Conclusion

I hope this post gave you a good understanding of cookies and also the MessageVerifier and MessageEncryptor APIs which have some great applications of their own outside of cookies. 

I'm not a cryptography expert and everything in this post was gleaned from looking at the Rails source code. So if something's unclear or I've got something wrong; write a comment and let me know!

railsrubysecuritywebdev
  • 0 0 Answers
  • 0 Views
  • 0 Followers
  • 0
Share
  • Facebook
  • Report

Leave an answer
Cancel reply

You must login to add an answer.

Forgot Password?

Need An Account, Sign Up Here

Sidebar

Ask A Question

Stats

  • Questions 4k
  • Answers 0
  • Best Answers 0
  • Users 1k
  • Popular
  • Answers
  • Author

    How to ensure that all the routes on my Symfony ...

    • 0 Answers
  • Author

    Insights into Forms in Flask

    • 0 Answers
  • Author

    Kick Start Your Next Project With Holo Theme

    • 0 Answers

Top Members

Samantha Carter

Samantha Carter

  • 0 Questions
  • 20 Points
Begginer
Ella Lewis

Ella Lewis

  • 0 Questions
  • 20 Points
Begginer
Isaac Anderson

Isaac Anderson

  • 0 Questions
  • 20 Points
Begginer

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore, ask, and connect. Join our vibrant Q&A community today!

About Us

  • About Us
  • Contact Us
  • All Users

Legal Stuff

  • Terms of Use
  • Privacy Policy
  • Cookie Policy

Help

  • Knowledge Base
  • Support

Follow

© 2022 Querify Question. All Rights Reserved

Insert/edit link

Enter the destination URL

Or link to existing content

    No search term specified. Showing recent items. Search or use up and down arrow keys to select an item.