Mitigating OWASP Top 10 Risks: Finding and Patching Insecure Deserialization in legacy session handling in Ruby
Understanding Insecure Deserialization in Legacy Ruby Session Handling
Many legacy Ruby applications, particularly those built on older versions of Ruby on Rails, relied on cookie-based session management. This often involved serializing session data (like user IDs, preferences, or temporary state) into a cookie, which was then sent back and forth between the client and server. The default serialization mechanism in older Rails versions often used Ruby’s built-in `Marshal` module. This presents a significant security vulnerability: insecure deserialization.
The `Marshal` module in Ruby is designed for serializing and deserializing Ruby objects. However, it’s not designed with security in mind. When `Marshal.load` (or its equivalent in Rails, `session[:key] = value` which implicitly uses Marshal for cookie sessions) encounters malicious serialized data, it can execute arbitrary Ruby code on the server. This is a direct path to Remote Code Execution (RCE), a critical vulnerability falling under OWASP Top 10’s “Identification and Exploitation of Vulnerabilities” (historically A1, now often covered by A03:2021 – Injection).
Identifying Vulnerable Session Handling
The first step is to pinpoint where your application is serializing and deserializing session data. In Ruby on Rails, this is most commonly found in the `config/initializers/session_store.rb` file. Look for configurations that specify `cookie_store` and, crucially, check if the `serializer` option is explicitly set or defaults to `Marshal`.
Example: Default Rails Session Store Configuration (Potentially Vulnerable)
A typical, older Rails configuration might look like this:
Rails.application.config.session_store :cookie_store, key: '_your_app_session', expire_after: 1.week
In this scenario, without an explicit `serializer` option, Rails defaults to using `Marshal`. This means that any data you store in the session (e.g., `session[:user_id] = 123`) will be marshaled into a string, base64 encoded, and placed in the `_your_app_session` cookie. An attacker can then craft a malicious payload, base64 encode it, and send it as the session cookie.
Crafting and Testing a Malicious Payload
To confirm the vulnerability, you can construct a simple Ruby script that uses `Marshal.dump` to create a malicious object. A common technique involves creating an object that, when loaded, executes a system command. For instance, a payload that attempts to run `id` on the server.
Example: Ruby Payload for RCE via Marshal
This script demonstrates how to create a payload that, when deserialized by `Marshal.load`, will execute a command. The `_load` method is a special hook in Ruby that gets called when an object is deserialized.
# malicious_payload.rb
require 'yaml' # Often used in conjunction or as an alternative, but Marshal is the focus here
# A simple class that executes a command when loaded
class Exploit
def initialize(cmd = 'echo "Vulnerable!"')
@cmd = cmd
end
def _load
system(@cmd)
# In a real attack, this might return a value or do nothing visible,
# but the side effect of system() is the exploit.
# For demonstration, we'll just print.
puts "Command executed: #{@cmd}"
return self # Return self to avoid errors if the application expects an object
end
end
# Create an instance of the exploit class
exploit_instance = Exploit.new('id') # Or 'ls -la', 'whoami', etc.
# Marshal the object
marshaled_data = Marshal.dump(exploit_instance)
# Base64 encode it, as cookies typically store base64 encoded data
encoded_payload = Base64.strict_encode64(marshaled_data)
puts "Encoded Payload:"
puts encoded_payload
Running this script will output a base64 encoded string. This string is your crafted session cookie. You would then use a tool like `curl` or a browser’s developer tools to set this cookie for your target application and observe if the command (`id` in this case) is executed on the server.
Testing with curl
Assuming your application is running locally on port 3000 and the session cookie name is `_your_app_session`, you would use:
curl -v -b "_your_app_session=YOUR_ENCODED_PAYLOAD_HERE" http://localhost:3000/
If the `id` command (or whatever command you chose) executes on the server, you’ll see its output in the `curl` response or potentially in the server’s logs, confirming the vulnerability.
Mitigation Strategies: Secure Serialization
The most effective way to mitigate this vulnerability is to stop using `Marshal` for session serialization. Modern Ruby on Rails versions have moved towards more secure serializers, or you can explicitly configure them.
1. Upgrade Rails and Use Default Secure Serializer
If you are on a sufficiently recent version of Rails (e.g., Rails 5.2+), the default session serializer is often `MGI` (MessagePack), which is generally considered more secure than `Marshal`. However, it’s crucial to verify your configuration.
2. Explicitly Configure a Secure Serializer
The recommended approach is to explicitly set a secure serializer in your `config/initializers/session_store.rb` file. The `MGI` (MessagePack) serializer is a good choice, or you can opt for JSON if your session data is simple and doesn’t require complex object structures.
Using MessagePack (MGI)
First, ensure you have the `msgpack` gem in your `Gemfile`:
# Gemfile gem 'msgpack'
Then, update your session store configuration:
# config/initializers/session_store.rb Rails.application.config.session_store :cookie_store, key: '_your_app_session', expire_after: 1.week, serializer: :message_pack
Using JSON
If your session data is simple (e.g., strings, numbers, booleans, arrays, and hashes), JSON is a safe and widely compatible option. Note that JSON cannot serialize arbitrary Ruby objects, which is a feature, not a bug, in this context.
# config/initializers/session_store.rb Rails.application.config.session_store :cookie_store, key: '_your_app_session', expire_after: 1.week, serializer: :json
After changing the serializer, you must restart your Rails application. Importantly, changing the serializer will invalidate existing sessions. Users will be logged out and will need to log in again. This is a necessary security measure.
3. Encrypting Session Cookies
Even with a secure serializer, it’s good practice to encrypt your session cookies. Rails provides built-in support for this. Ensure you have a strong `secret_key_base` configured in your `config/secrets.yml` (or environment variables for newer Rails versions). The default `cookie_store` in Rails automatically encrypts the cookie content using this secret key.
To verify encryption is active, check your `config/initializers/session_store.rb`. If you see `Rails.application.config.action_controller.session = { … }` or similar configurations that don’t explicitly disable encryption, it’s likely enabled. The `cookie_store` itself handles the encryption and decryption lifecycle.
Alternative: Server-Side Session Storage
For applications with sensitive session data or very large session payloads, relying on cookie-based sessions at all can be risky. A more robust approach is to store session data server-side and only use a secure, opaque session ID in the cookie.
Using Redis or Memcached for Sessions
Rails supports various session stores, including `redis-session-store` or `memcached-store`. This involves configuring your application to use a dedicated session backend.
# Gemfile gem 'redis' # or gem 'dalli' for Memcached gem 'redis-session-store' # or use the built-in memcached store
# config/initializers/session_store.rb
# For Redis
Rails.application.config.session_store :redis_session_store,
redis: {
host: ENV.fetch('REDIS_HOST', 'localhost'),
port: ENV.fetch('REDIS_PORT', 6379),
db: ENV.fetch('REDIS_DB', 0)
},
key: '_your_app_session',
expire_after: 1.week
# For Memcached (using built-in support)
# Rails.application.config.session_store :mem_cache_store,
# :key => '_your_app_session',
# :memcache_server => ['localhost:11211'],
# :expire_after => 1.week
With server-side session storage, the cookie only contains a randomly generated ID. The actual session data is stored securely on the server, significantly reducing the attack surface for deserialization vulnerabilities.
Patching and Remediation Workflow
- Audit: Review `config/initializers/session_store.rb` and any custom session middleware for the serialization method used.
- Test: If `Marshal` is suspected, attempt to craft and send a malicious cookie to confirm the RCE vulnerability. Use tools like Burp Suite or OWASP ZAP for more sophisticated testing.
- Remediate: Update `config/initializers/session_store.rb` to use `:message_pack` or `:json` as the serializer. Ensure the necessary gems are installed.
- Deploy: Restart the application. Be aware that this will invalidate existing user sessions.
- Verify: Re-test the application to ensure the vulnerability is no longer present. Check server logs for any unexpected deserialization errors.
- Consider Server-Side Storage: For critical applications or those handling highly sensitive data, migrate to server-side session storage (Redis, Memcached) for enhanced security.
By diligently identifying and patching insecure deserialization in legacy session handling, you can significantly strengthen your application’s security posture against critical OWASP Top 10 threats.