Original research by Md. Asif Hossain — read the full writeup here: https://medium.com/@0x0Asif/your-session-is-just-a-post-body-and-the-secret-that-signed-it-was-shared-across-environments-e5f5266566e0 · Researcher's profile: https://medium.com/@0x0Asif
Analysis by Faizan Akhtar.
One request. Zero credentials. A complete stranger becomes a support-tool administrator who can act as every customer at once — reading 13,000+ accounts across tenants, creating Premium-entitled users, minting admin sessions for any identity he names, and pivoting into the company's internal service mesh. That is the chain Md. Asif Hossain documented in a mid-October 2026 writeup based on an authorized, coordinated assessment of a multi-tenant education platform (vendor, hosts and endpoints anonymized; the tell-tales are parent↔student email mappings, per-tenant admin roles, and paid Premium entitlements). Five findings link into one escalating story — and the most instructive part is how a "fixed" vulnerability survived its own fix. This is my original analysis of what broke, the reasoning that exposed it, and what hunters and defenders should carry away.
Background
In PHP, "being logged in" is a pointer plus a server-side blob. The browser holds a PHPSESSID cookie; the server keeps the matching session store; session_start() loads that blob into the $_SESSION superglobal; and every guard reads from it. The entire model rests on one assumption: the client can only influence $_SESSION indirectly. The moment a client can write into $_SESSION directly, every downstream auth check becomes attacker-controlled input.
The target is a multi-tenant education service with a support console that is cross-tenant by design — its whole job is to reach all customers. Inside it sat an internal "manual import" handler: a utility endpoint built for operations staff, wired to a test shim. The writeup is explicit that this was an authorized assessment with tight safety discipline — writes hit only controlled test identifiers, every mutation was reverted, and customer PII is redacted. What follows is an analysis of the findings, not a reproduction of the requests.
The Vulnerability
Five linked findings — the first two are the core; the other three are what the core unlocked.
1. Pre-auth session forgery — an auth-ordering inversion. The import handler started the session, decoded the JSON body, and copied the client-supplied session block straight into $_SESSION when a flag was set — before loading the auth gate. The gate then judged email and role on data the requester had just written: setup ran before authentication (CWE-501 → CWE-287), so the gate certified the forgery. Two details carry the lessons. The role check demanded the integer 0 (strict comparison), and PHP's json_decode preserves types — a body carrying the number 0 passed while "0" failed. And the request did not even need to "succeed": the handler issued the cross-tenant support_tool_bearer cookie before its own XSRF/validation check rejected the request. HTTP 400 came back carrying a working fleet-wide credential in Set-Cookie. The status code lied; anyone who stops reading at the status line misses the finding.
2. Cross-environment session acceptance — the fix that fixed nothing. The vendor patched the forge path on production, which exposed the second flaw: the QA/stage cluster still hosted the same shim, the bearer was signed with a secret shared across QA/stage and production, and the cookie was scoped to the apex domain. A QA-minted credential was therefore honored as a support-tool admin session on production — cookie scope carried it to every subdomain, and fleet-wide signature trust validated it since the token carried no environment binding. Fixing production while the same artifact ran on QA was cosmetic.
3–5. What the bearer unlocked. Cross-tenant read: the support console's read handlers returned the federated roster — 13,000+ accounts across all clusters, 3,001 in a single cluster — plus per-tenant config and parent↔student email mappings, no ownership checks. The causality control makes it undeniable: the same handler with no cookie returned 401 "BAD TOKEN FORMAT" — proving the endpoint is genuinely authenticated and the token is real (and incidentally disclosing verbose errors). Cross-tenant write: the same bearer drove mutating handlers accepting a client-supplied tenant ID with no permission gate or tenant-ownership check — alias admin accounts with Premium entitlement bits set, verified, then reverted. And SSRF: a user-controlled cluster parameter drove server-side requests from the support tool, and the response echoed the internal support API's own auth token back — CWE-918 with credential disclosure, a foothold inside the internal service mesh.
How It Was Found
The writeup does not narrate the recon path to the shim — it documents the reasoning chain that makes the find reproducible. Reconstructed from the findings:
The starting question: what happens before the auth check? Utility endpoints — manual imports, restore-debug-session, re-hydrate-state handlers — are written for authenticated admins, given no auth of their own, then exposed by a routing rule or leftover test shim. Enumerate those handlers and test each against the ordering invariant.
The payload work shows type discipline: crafting the forged block around PHP's strict comparison and json_decode's type fidelity. The observation work is sharper: noticing a 400 response carried a Set-Cookie, and grasping what it meant — the credential is minted before validation fails. Most testers read status codes; this one read headers.
Then the deployment-awareness most reports skip. The platform sat behind an AWS ALB with sticky sessions, and the forged session lived on exactly one backend node — replaying only PHPSESSID could land on a node that had never seen the forgery and return 401. The fix: carry the ALB's own cookie forward in a cookie jar. And after the vendor announced the production fix, the reasoning did not stop at "patched" — it asked where else the code runs. QA still hosted the shim. Then it asked what the token was worth beyond the session: chasing the bearer to production, where shared signature trust and apex cookie scope turned a QA artifact into production admin access. Chase the token, not the session.
Why It Worked
Three layers trusted something they should not have, and an operational failure cemented all three. The handler trusted request data as session state — setup-before-auth meant the gate certified forged input. The deployment trusted the environment boundary — a shared signing secret plus an apex-scoped cookie erased the line between a test cluster and the live fleet. The support tool trusted its network position — cross-tenant by design, with handlers that never checked tenant ownership, because "internal" was treated as safe.
And the operational failure: the fix addressed one environment while the identical artifact remained reachable in another. Multi-tenancy was the amplifier — a session bug on a single-tenant app compromises one account; the same bug behind a cross-tenant support console, a tool whose purpose is reaching every customer, becomes fleet-wide. The writeup's summary is precise: unauthenticated, self-contained, identity-granting, fleet-wide. One request converted a stranger into an administrator who could act as every customer at once.
Lessons for Hunters
Ask "what happens before the auth check?" on every endpoint. The single most valuable review question in the writeup; it generalizes across languages: find the gate, then read upward.
$_SESSION = $request is a pattern-matchable red flag — but grep smarter. The naive grep for direct $_SESSION assignment misses the common real form: a keyed loop copying each input key into the session, plus array_merge, extract($_POST), parse_str, session_id($_GET[…]), unserialize, and framework wrappers. A hit is a signal, not a verdict — for each one, answer two questions: where does the data come from, and is the auth check before or after this line?
Never trust the status code. A 400 carried a working fleet-wide credential here. Read the full response on failures — the security outcome can diverge from the HTTP outcome.
Carry the load balancer's cookies. Sticky sessions pin forged state to one backend node; replay the LB cookie in a cookie jar or your exploit "mysteriously fails" — and mysterious failures get abandoned.
After "fixed on prod," ask where else the code runs. QA/stage drift, leftover clusters, and forgotten subdomains are where theoretical becomes live — a credential minted in one environment may be honored in another when secrets are shared.
Prove causality, then write it up. The 401-without-cookie control makes the finding undeniable — and its verbose error was a bonus finding.
Mitigation
The structural fixes, distilled from the writeup's remediation section:
- Never assign request data to $_SESSION wholesale — set only specific, validated keys, and only after authentication succeeds. Enforce the ordering invariant (authenticate → authorize → then mutate) in middleware.
- Store only an opaque user ID in the session; re-derive role, tenant, and email from a trusted store on every request. Never carry authorization data in a session blob that can be minted elsewhere.
- Remove test shims from every environment, not just production — grep all clusters for the harness artifact and verify it 404s in the release checklist.
- Bind token trust to the issuing environment: never share a signing secret across QA/stage/prod — use per-environment keys, put iss/aud/env/tenant claims in the token, validate them everywhere, and scope session cookies to the narrowest host.
- Harden internal cross-tenant tooling as if it were public: per-endpoint permission gates, tenant-ownership checks on every read and write, and logging/alerting on every impersonation action.
- Detect the class live: alert on $_SESSION mutation on an unauthenticated request, plus standard session hardening (strict mode, secure/HttpOnly/SameSite cookies, regenerate ID on privilege change).
If you found this breakdown useful, read the earlier entry in this writeup-analysis series: my analysis of how Hacktron chained a malicious HEIF image through an SSO flaw to internal repo access at OpenAI — another story of trust crossing a boundary it should never have crossed — https://faizanakhtarcyber.blogspot.com/2026/10/openai-libheif-sso-account-takeover-analysis.html.
Report by Faizan Akhtar — The Cyber Security Researcher
Top comments (0)