Sessions and identity
Published
A session middleware is configured almost entirely by narrowing its defaults: your own identifier generator instead of the library's, a shared store instead of process memory, a new identifier when a visitor signs in, cookie flags the default does not set. All of those are property names on an options object, and the middleware reading them was installed rather than built. We protected a file that configures one, renamed the option names a group at a time, and counted what an attacker could still do.
What the sample actually does
The file configures a session middleware with nine options and then puts five requests through it, in the order a real session fixation happens. An attacker visits the site and does something that writes to their session, so the middleware stores it and hands back a real identifier. That identifier is planted in the victim's browser. The victim signs in while presenting it. The attacker then replays the identifier they planted. A fifth request replays the victim's own identifier against a second worker process, which is the only thing that tells you whether the store is shared.
The middleware is copied into the measurement directory unprotected, because that is the shape of the real thing. Your bundle is rebuilt when you protect it; the package in node_modules is not, and it keeps reading the property names it has always read. Its defaults are the permissive ones this family of libraries ships: a short counter-derived identifier, an in-process memory store, no new identifier at a privilege change, a session record for every anonymous visitor, and a cookie with a path and nothing else.
Protection alone was applied first, on five presets covering both output targets, the gate profile and the compressed profile. All five behaved identically to the unprotected file. Nothing below is caused by protection on its own. Every result required member renaming aimed at the option names, which is what the MemberRegexp option exists to scope.
The attacker's own cookie became the victim's session
Renaming the option that asks for a new identifier at a privilege change is the sharpest result in this area, and it is the textbook attack rather than a novel one. In the unprotected run the login retires the presented identifier and issues a fresh one: the attacker replays what they planted and gets an empty anonymous session, privileged=false, with no user attached.
With that one option name renamed, the login keeps the identifier the caller presented. The replay reported privileged=true and the user on it was the victim's address. The application's own summary lines moved from planted-id-still-privileged=false to true and from planted-id-now-belongs-to=null to the victim's email address. No error was raised at any point, and the victim's experience is a completely normal sign-in.
It is worth being precise about who has to do what. Session fixation needs the attacker to plant an identifier, which is a separate problem you may or may not have: a subdomain that can write cookies for the parent domain, a URL parameter the application accepts, or an XSS bug. The regeneration option is the control that makes all of those harmless afterwards. Losing it does not create the plant, it removes the thing that made the plant worthless.
The middleware still reports success, still writes a session, still sets a cookie. There is no log line anywhere that says the identifier was reused, because reusing an identifier is what the library's default does and defaults are not warnings.
Identifiers stopped being unguessable
The application supplies its own identifier generator. With that option name renamed, the library used its built-in, and the identifiers in the output went from a 22-character random body to sid1, sid3, sid4. The measurement line the application prints about itself moved from session-id-random-chars=22 to 1.
A sequential identifier means the session belonging to the next person to sign in is one increment away from yours. It needs no interception, no cross-site request and no cookie plant. It is also the single easiest thing in this article to see if you go looking, and the single easiest to miss if you do not, because a session identifier is not something anybody reads once the feature works.
This is the general shape the whole series keeps finding, stated for identifiers: a library's default has to work for an application that configured nothing, so it is chosen for compatibility rather than for strength. Your configuration is the strength. When the name of the configuration is lost, you are back on the default, and the default was never meant to be your security boundary.
The store, and the object that stopped looking like a store
Two arms in this area produce the same outcome by different routes, and the second one is the more interesting.
The obvious one: rename the store option and the middleware falls back to its own in-process memory store. The replay of the victim's identifier against the second worker went from a signed-in session to nothing, and the store the middleware reported for itself went from shared-redis to memory(w1). That is an availability failure people notice quickly, because signed-in users are logged out whenever a load balancer sends them to a different process.
The second one is quieter to reason about. Leave the option name alone and rename the METHOD names on the store object you passed - read, write, drop, size. The middleware checks that what it was handed looks like a store before using it, the check fails because the methods are no longer called what the check looks for, and it silently substitutes its own. Same fallback, same reported memory(w1), but nothing about the option itself was touched. An exclusion list built around option NAMES does not cover the SHAPE of the object those options carry.
That generalises well beyond sessions. Any time you hand a library an object it duck-types - a store, a logger, a transport, a cache - the method names on that object are part of the contract just as much as the key it arrives under.
Cookies, anonymous visitors, and the flags that quietly left
Renaming the cookie options object took the emitted header from __Host-sid=...; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=strict to __Host-sid=...; Path=/. The session still works perfectly. It is now readable by script, sent over plaintext, and attached to cross-site requests.
Renaming the cookie NAME option is smaller and worth knowing about: the name went from __Host-sid to the library default connect.sid. The __Host- prefix is not decoration - browsers enforce rules on any cookie carrying it, including that it must be secure and must not carry a domain attribute, which is exactly the protection that stops a sibling subdomain writing the cookie in the first place. Losing the prefix quietly gives back the property that makes a planted cookie possible, in the same build as the arm that makes a planted cookie useful.
Renaming saveUninitialized restored the default, and every anonymous visitor started getting a stored session record and a cookie. That is a storage and privacy question rather than an attack, and it is the arm most likely to be noticed first, because the store fills up with records nobody asked for.
Two options were pinned to values identical to the library default on purpose, as controls. Both behaved exactly as predicted: renaming them changed nothing at all, for the seventh consecutive pass in which that prediction has held.
What to do about it
The mechanism is not specific to sessions and it is not a defect in the obfuscator. Member renaming rewrites property names inside the code it is given. An installed middleware is not inside that code, so a renamed option name is a name it has never heard of, and every mature library ignores what it does not recognise and applies its documented default.
The practical control is scoping. RenameMembers takes a MemberRegexp, and the options object handed to a session middleware belongs outside it - together with the method names of any object you pass through that options object. If you would rather not maintain an exclusion list, build the configuration with quoted string keys and read it with bracket access using literal strings, or move it into a JSON file the build does not transform.
The verification that catches every arm here is a start-up assertion, not a test. Ask the middleware what it is using: the store it selected, the identifier length it produced, whether the cookie header it emits carries the flags you configured. Fail the boot if any of them is not what you asked for. A functional test that signs in and out passes on every arm in this article, including the one where the attacker's planted identifier is now the victim's session, because signing in and out is exactly what still works.
Frequently asked questions
Does protecting my JavaScript break session handling on its own?
Not in this measurement. The sample was protected on five profiles covering both output targets, the gate profile and the compressed profile, and all five behaved identically to the unprotected file. Every result in this article required member renaming pointed at the option names.
Which single option was the worst to lose?
The one that issues a new session identifier when a visitor signs in. Without it, an identifier an attacker planted before the login is still valid after it, and the replay reported the victim's account. Everything else in the area is either louder or narrower.
Why does a renamed option name not raise an error?
Because a middleware receives an ordinary object and reads the property names it knows. A name it has never heard of is not an error, it is simply absent, so the documented default applies. Refusing unknown keys would break forward compatibility, so mature libraries do not do it.
I only excluded the option names from renaming. Is that enough?
Not if any option carries an object the library duck-types. Renaming the read and write methods on the session store made the middleware reject it as not a store and fall back to process memory, without the option name being touched at all.
Would our tests have caught this?
A sign-in and sign-out test passes on every arm measured here. What catches it is asserting the configuration the middleware reports about itself at start-up: the store it chose, the length of the identifiers it generates and the flags on the cookie it emits.
Does a session fixation problem need obfuscation to happen?
No. It needs an attacker to plant an identifier, which is a separate weakness. What the renamed option removes is the control that makes a planted identifier worthless afterwards, so the two combine rather than either one being sufficient.
What is the recommended fix?
Scope member renaming so that middleware options objects and the objects they carry are excluded, or write those objects with quoted string keys and read them with literal bracket access. Then add the start-up assertion above so a later configuration change cannot regress silently.
Related reading