Gotchas with Rails hybrid cookie serialization

Lily Reile
2 min readJul 1, 2021

Problem

We recently upgraded one of our apps to Rails 6, which required upgrading Brakeman to 5.x. Brakeman promptly threw this warning:

Use of unsafe cookie serialization strategy `:marshal` might lead to remote code execution

My understanding is that Marshal deserializes complex objects by effectively evaling them as arbitrary Ruby code. The concern here is that an attacker could craft a cookie value that when deserialized would execute a payload on the server.

This actually happened back in the Rails 4.x days, and those particular vulnerabilities were patched on subsequent releases. As far as anyone knows, there are no active vulnerabilities with Marshal, but the Rails and Brakeman teams felt that moving away from it would be good defense-in-depth.

Solution

At this point you should read this excellent article from Big Binary on migrating from Marshal to JSON strategies, then come back here.

Gotchas

We encountered some gotchas that were not mentioned elsewhere on the Internet:

Symbols or Strings?

Consider this hash: { foo: 'bar' }. When this hash is serialized by Marshal, the symbol is preserved. Deserializing it yields { foo: 'bar' }.

However, when it’s serialized by JSON, the symbol is stringified. Deserializing it yields { "foo": "bar" }.

This is a problem because depending on when the cookie was stored, it could have either strings or symbols for keys when it’s retrieved. We worked around this by casting the retrieved hashes to HashWithIndifferentAccess. We’ll remove that cast once we’ve migrated to the JSON strategy.

ActionController::Parameters or Hash?

If your app stores raw params in the session for some terrible reason (e.g., multi-step forms), they’ll come out as different types depending on when the cookie was stored.

Due to strong parameters, params are now of the type ActionController::Parameters. Our app was directly serializing these params (with Marshal), and on deserializing them they were stillActionController::Parameters. This meant that we needed to call permit on those params before accessing them.

However, serializing them as JSON did not preserve that complex type. New cookies were being deserialized as Hash instead. This of course led to NoMethodError (undefined method `permit’ for {}:Hash).

We worked around this by temporarily checking the type:

retrieved_params = session.delete(:stored_params)# TODO: delete this block once we're on the JSON cookie serialization strategy
if retrieved_params.respond_to?(:permit)
retrieved_params = retrieved_params.permit(:foo)
end
[...]

I recommend never directly storing complex types in order to avoid gotchas like this.

--

--