Compatibility
Published
A cookie's security does not live in the value. It lives in the attributes beside the value: HttpOnly keeps it away from script, Secure keeps it off plain HTTP, SameSite decides which navigations carry it, Max-Age decides whether it survives the tab. Every one of those is a property on an options object that your code hands to a serializer, and the far side of that handoff is a browser that reads only the header text. That makes the options object a contract with names somebody else fixed.
What was measured
One file, driven through five protection profiles for the base column and both member-renaming profiles for everything after. It sets the three cookies every application sets: a session cookie with HttpOnly, Secure, SameSite Strict, an hour of Max-Age and a path narrower than the origin; a cross-site embed cookie that uses SameSite None, which browsers only accept when Secure is also present; and the deletion header that logout sends. The serializer is a separate module copied in unprotected, with the option names every real one uses.
The base column is clean. On the default target, the modern target, the gate profile, the modern gate profile and the string-transform profile, the protected build produced the same Set-Cookie header, byte for byte, and the same decisions on the receiving side. Protecting an application that sets cookies does not disturb the cookies.
Everything below is the member-renaming column. The value that crosses the boundary here is the header text, so the header is printed verbatim in every run rather than inferred from the object that produced it. That distinction turns out to be the practical advice at the end of this page.
The flags do not become wrong, they stop being written
A pattern matching the HttpOnly key produced a header that reads sid, Max-Age, Path, Secure and SameSite Strict, and nothing else. The attribute is not misspelled and not set to false. It is absent, because the serializer asks the options object for a name the object no longer has, gets undefined, and skips the branch that appends the attribute.
The consequence measured on the receiving side: a session cookie that was unreadable by page script became readable by page script. That is the single control standing between a cross-site scripting bug and session theft, and it was removed by a build step, in a codebase where the line setting it is still there and still says true.
The Secure arm behaved the same way and cost two things at once. The header lost the attribute, so the cookie is now sent over plain HTTP if anything ever downgrades a request. And the cross-site embed cookie, which pairs SameSite None with Secure, became a header the browser rejects outright, because None without Secure is not a valid combination. One renamed read produced both a weakened cookie and a cookie that never gets stored at all.
The SameSite arm dropped the attribute entirely, which does not disable the protection so much as hand it to the browser's default. Modern browsers treat an attribute-less cookie as Lax, so a Strict cookie quietly becomes Lax, and the None cookie loses the cross-site behaviour the whole embed depends on.
Logout stops logging anybody out
The quietest result came from the Max-Age arm, and it is quiet because both halves of it look like success.
The first half is ordinary: a cookie that was meant to last an hour is emitted with no Max-Age, so it becomes a session cookie. Users are signed out when they close the browser, which reads as a session-length policy change and gets investigated as one.
The second half is the one worth stopping on. Deleting a cookie is not a delete operation. It is the same Set-Cookie header with an expiry already in the past, which in this code is Max-Age zero. Renaming the key removes the zero along with everything else, so the logout header no longer expires anything. Measured directly: the expiring-header check went from true to false. The route still returns 200, the client-side state still clears, and the cookie is still in the jar.
That is the general shape of this class. A safety attribute's default is the unsafe value, because the safe behaviour is the one you opt into. Renaming an ordinary option gives you a wrong-but-harmless default; renaming HttpOnly, Secure or an expiry gives you exactly the thing the attribute exists to prevent.
Scope creep, and the one arm that throws
A pattern matching path and domain widened the cookie rather than breaking it. The session cookie was scoped to a single application path; after renaming it is emitted with the serializer's default path of slash, so it now travels with every request to the origin, including to unrelated applications sharing that host. Nothing failed, and the cookie works better than before, which is precisely the problem.
The loud arm is the one that matches the serializer's own method names. Renaming those produced an immediate TypeError naming a generated identifier, on the first line that sets a cookie, in every environment including the developer's. That is the good failure mode and it argues for the same mitigation the rest of this series recommends: anchor a member pattern to names you own rather than enumerating names to avoid.
Between those two sits the control arm, which renamed the audit record the application logs after setting a cookie. The behaviour was unaffected and the log line became generated identifiers, so the one artifact an operator would read to reconstruct what happened is the artifact that stopped being readable.
The security self-check that reported nothing
This file contains two checks, of the kind a reviewer adds after an incident. The first reads the options object and reports which protections are missing. The second reads the emitted header and reports the same thing.
In the run where every attribute had been stripped from the header, the first check reported no findings. It is not broken and it is not lying: it reads the same renamed property the serializer failed to read, both ends of that read moved together, and by its own lights the configuration is correct. The second check reported HttpOnly and Secure as missing, correctly, because the header is text and text is not a rename site.
That is the transferable rule, and it is cheap to act on. Assertions about a header, a document or a payload have to run against the emitted bytes, not against the object you built them from. A test that says the options object has HttpOnly set will pass forever. A test that says the Set-Cookie header contains the string HttpOnly catches this in the first run of the suite.
What to do about it
Protection alone does not touch any of this, on any profile measured, so nothing here is a reason to avoid protecting an application that uses cookies. The exposure is member renaming with a pattern broad enough to reach the attribute names, and the fix is scoping rather than avoidance.
Keep HttpOnly, secure, sameSite, maxAge, path and domain out of any member pattern, along with the method names of whatever writes the header. Better than a list of exclusions: anchor the pattern to a private naming convention of your own, so no name fixed by a specification or by a dependency can be caught by accident.
Then add one integration test that reads the header. Set a cookie, capture the response, and assert on the substrings the browser will act on. It is three lines, it survives every future refactor of the options object, and it is the only check in this measurement that noticed anything was wrong.
Frequently asked questions
Does obfuscation break cookies?
Not in the default configuration. A file that sets a session cookie, a cross-site embed cookie and a deletion header produced identical output on all five protection profiles measured. Cookie attributes become a surface only when member renaming is switched on and the pattern reaches the option names the serializer reads.
Can member renaming remove HttpOnly from a cookie?
Yes, and that was measured. With a pattern matching the HttpOnly key, the emitted Set-Cookie header no longer carried the attribute, and the receiving side went from a cookie unreadable by page script to one readable by page script. The line setting it is unchanged in the source; the serializer simply asks for a name that is no longer there.
What happens to SameSite and Secure after renaming?
The attributes are omitted rather than set wrongly, so the browser applies its own defaults. A SameSite Strict cookie becomes an attribute-less cookie that browsers treat as Lax. A cookie that pairs SameSite None with Secure becomes an invalid combination and is rejected outright, so a cross-site widget loses its session entirely.
Why did logout stop clearing the session cookie?
Because clearing a cookie is a Set-Cookie header with an expiry in the past, usually Max-Age zero. When the Max-Age key is renamed, the value never reaches the header, so the deletion header expires nothing. Measured directly: the expiring-header check went from true to false while the logout route still returned success.
Why does our own security check report that the cookie is fine?
Because it reads the same options object the serializer failed to read, and both ends of that read were renamed together. In the measured run the object-based check reported no findings while the header had lost HttpOnly and Secure. The header-based check in the same file reported both, because text is not a rename site.
Which cookie names should stay out of a member renaming pattern?
httpOnly, secure, sameSite, maxAge, expires, path, domain, partitioned and priority, plus the method names of whatever serializes them. Anchoring the pattern to your own naming convention is stronger than maintaining that list, because the list grows every time a specification adds an attribute.
How do we test for this on a protected build?
Assert on the emitted header rather than on the object. Set a cookie in an integration test, capture the response header and check that it contains HttpOnly, Secure and the SameSite value you expect. That check was the only one in this measurement that noticed anything had changed.
Related reading