Start your free trial — no credit card required.
Entra MFA Sidestepped: A Forged Cookie Unlocked 95 Accounts and a Super User in a Supply-Chain App
Neha10 min read

Entra MFA Sidestepped: A Forged Cookie Unlocked 95 Accounts and a Super User in a Supply-Chain App

A yard management app had Entra ID SSO and MFA. Its session cookie was signed with a guessable secret, and testers impersonated 95 of 241 users without logging in.

Share:

Ninety-five out of 241. That's how many employee accounts Resecurity's testers could impersonate in a supply-chain yard management system, according to the firm's write-up published on 1 October. The app sat behind Microsoft Entra ID single sign-on with MFA turned on. None of that mattered. The testers never typed a password, never approved a push and never touched an Entra token. They made their own session cookie, and the server accepted it.

Cyber Security News picked up the report on 2 October, and the numbers match across both: 241 user IDs tried, 95 distinct accounts impersonated, one of them an administrator with the role SUPER_USER. A forged admin session made a state-changing API call, and the change stuck. The same forged cookie worked on the API host and on the separate admin console host.

This was an authorized assessment against the vendor's staging environment. Records were restored afterwards, and Resecurity anonymized the vendor and the identities. So nobody is filing a breach notice over this one. I still think it's one of the most useful identity stories of the month, because the flaw isn't exotic and I've seen its cousins in production more than once.

The front door was fine

Let's give the app credit first, because Resecurity did. Sign-in went through Azure Entra ID via MSAL. Access tokens were RS256-signed. Requests were checked against an OpenAPI schema with express-openapi-validator. Database access went through Prisma against PostgreSQL, with parameterized queries. If you scanned this stack for the usual sins, such as SQL injection, unsigned JWTs or a missing MFA policy, you'd have found very little.

Then look at what happened after a user signed in. The Node.js/Express backend issued its own session cookie, and from then on that cookie was the thing that granted access. Entra did its job once at the start. The cookie did everything after that.

This pattern is everywhere. Teams wire up SSO, tick the "MFA enforced" box in Conditional Access, and then hand off to whatever session handling the web framework gives them. The identity provider protects the login. It doesn't protect the session your app mints afterwards, and it can't.

Anatomy of the forgery

Two mistakes stacked on top of each other. Either one alone would have been survivable.

Mistake one: the secret. The cookie used the familiar Express signed-cookie format, s:<payload>.<signature>, where the signature is an HMAC-SHA256 of the payload, base64-encoded with the padding stripped. That's a sound design if the HMAC key is secret. Here, according to Resecurity, "the session signing secret was hard-coded to the same value as the cookie name." In the published version both read session_secret_example. Given the anonymization the literal string may be a placeholder, but the root cause is stated plainly: the key was the cookie's own name.

How did the testers find it? They captured one legitimate cookie and ran an offline search over about 110 candidate secrets. There was no rate limit to beat and no lockout, because offline guessing never talks to the server. You compute the HMAC for each guess and compare it to the signature you already have. A hundred and ten guesses takes less time than reading this sentence.

Mistake two: the payload. A signed cookie should carry a random session ID that points at state stored on the server. This one carried the user's database ID, a CUID. CUIDs are collision-resistant, not secret, and this app handed them out freely. Lookup endpoints like /admin/lookups/app-users and /admin/lookups/employee-users returned them in API responses, and an unauthenticated Swagger UI at /api-docs/ documented all 251 routes so you'd know where to look.

Put those together and the attack is three steps. Read a user's ID from an API response. Sign it with the known secret. Send the cookie. The server checks the HMAC, it's valid, and you're that user. In Resecurity's own words, "the application treated a client-controlled, self-contained signed value as sufficient proof of identity."

One more line in the report deserves attention. Resecurity says /api/v1/auth/me reportedly returned the user's Entra refresh token. If that holds, a forged cookie for the app didn't just stay in the app. A refresh token can be redeemed for new access tokens, so an attacker could move from one sloppy internal tool back into the tenant, carrying a real Microsoft-issued credential. That's the moment a staging finding becomes a tenant-wide problem.

Why 95 and not 241?

The report gives the count but not a breakdown, so I'll be careful here. Not every ID you try maps to an active account with a session the app will honor: some will be disabled, deprovisioned or limited to roles that never touch that host. What matters is the shape of the result. Nearly 40% of the tested population, across roles from Yard Marshal and Technician up to super user, fell to the same trick. There was no privileged path that needed a second exploit. The admin was just another ID.

Three things people get wrong about this class of bug

"MFA would have stopped it." MFA was on. MFA protects the act of authenticating. Once a session exists, whether it was earned or forged, MFA is out of the picture. This is the same lesson session-cookie theft has been teaching for years: stealing a valid cookie and forging one end up in the same place. We covered the theft version when TA419's browser-in-the-browser kit clicked "Stay signed in" on victims' behalf to harvest long-lived cookies. Forgery is worse in one respect. It doesn't need a victim to click anything.

"Our Entra sign-in logs would show it." They wouldn't. A forged app cookie never calls Entra. There's no sign-in event, no risky sign-in score, no Conditional Access evaluation, no impossible-travel alert. From Microsoft's side of the fence, nothing happened. Your only record lives in the app's own logs, and in most internal tools those logs say "user X called endpoint Y" and nothing about how user X's session started.

"It's an internal app, so the blast radius is small." Yard management systems schedule trailers, assign dock doors and track what's sitting in the yard. That's operational data a cargo thief would love to read, and an admin who can change it can change what happens at the gate. Internal tools also tend to be the ones built fastest, by teams copying setup code from tutorials. And if the refresh-token exposure is real, "internal" stops meaning contained.

The tutorial problem

Here's the part that comes from doing this work rather than reading about it. When I review Express apps, the first thing I search for is the literal string keyboard cat. That's the example secret in the express-session README, and it ends up in production far more often than anyone admits. Close behind it come secret, changeme, the app's own name, and, as this case shows, the cookie name.

A list of 110 candidates sounds small. It's enough because the real-world set of lazy secrets is small. Anyone who has done a few of these reviews could write that wordlist from memory: framework defaults, README examples, the project name, the cookie name, the environment name, and a few of those with 123 on the end.

The other habit worth searching for is any code that puts an identifier you can enumerate, such as a user ID, an email address or an employee number, directly into a cookie and trusts it because it's signed. A signature proves who made the value. It says nothing about whether the value should be secret. If your signing key leaks, every enumerable ID becomes a login.

This is the same family of failure as the one in Storm-3168's service principal attack, where a secret that should have lived in a vault sat in a GitHub edit history. Signing keys are credentials. They belong in a secrets manager, separate per environment, rotated, and never in source code.

What I'd check this week

Resecurity's remediation list is sensible, and you should run it as written: rotate the secret and kill existing sessions, generate a strong random key, stop using user IDs as session values, back sessions with a server-side store like Redis, separate secrets per environment, and review logs for odd session creation and admin actions. A few additions from my side:

  1. Grep every Node service for its session secret. Search for keyboard cat, secret:, cookieParser( with a string literal, and any secret equal to the cookie name. Ten minutes, and it'll probably find something.
  2. Close unauthenticated API docs in non-dev environments. A public Swagger page isn't a vulnerability by itself. It's a map, and this attacker used it.
  3. Audit what /me endpoints return. If any endpoint reflects an IdP refresh token, or any token at all, to the browser, treat it as a P1 finding.
  4. Log how a session started, not only what it did. Record the Entra sign-in correlation ID when you mint the app session. A session with no matching IdP sign-in is a forged session. That one join would have turned this whole attack into an alert.
  5. Inventory the apps that sit behind SSO but run their own sessions. That's most of them. The Lenovo ID and Dropbox incident showed how much trust lives in federation glue nobody owns. Session handling after SSO is the same blind spot from the other side.

Where deception fits

Point 4 is the clean fix, but it needs code changes in every app, and you won't get them all this quarter. There's a cheaper tripwire that works today, and it uses the attacker's own method against them.

The forgery depended on harvesting real user IDs from lookup endpoints. So plant identities that only exist to be harvested. Create a handful of decoy user records in each internal app: plausible names, plausible roles, a SUPER_USER or two. Give them no Entra account, so nobody can ever sign in as them legitimately. Their IDs show up in the same API responses as everyone else's. If any session, cookie or API call ever claims to be one of those users, you know with certainty it's forged or replayed, because no human holds that identity. There's no tuning and no baseline. It's either zero events or an incident.

That's the idea behind the honeytokens we build at Mine2, a credential or identity that has no legitimate use, so any use at all is a signal. You can extend it the way this case suggests. Seed a decoy refresh token where an /auth/me-style endpoint would leak it, and watch the tenant for anyone trying to redeem it. The attacker who's sure they've escaped your Entra logs then shows up in them, at a moment you chose. Decoy identities also don't need application code changes, which is why identity-focused deception is usually the fastest layer to add to an SSO estate that's full of homegrown session handling.

Resecurity got there first on a staging box, with permission. The next team to find a cookie signed with its own name may not ask first. Book a Mine2 demo and we'll show you where decoy identities would have caught this forgery on its first request.

M2

Neha

Cloud Security Architect, Mine2

Neha works on cloud and identity security at Mine2, covering SaaS, OAuth, and the credential-theft paths attackers favour in the cloud.

Share this article

Secure Your Network Today

Ready to implement advanced cyber deception in your organization? See how MINE2 can transform your threat detection capabilities.