It Validated Every Claim and Never Checked the Signature
On the first Saturday of September, at around one in the morning, a researcher changed a single string in a login token that was never signed. The field was upn, which stands for User Principal Name and is normally an email address. He replaced the email shape with the word admin. The service returned the number 1, and then it ran his SQL.
The service was Titan, Microsoft’s internal analytics platform. It had checked his token’s tenant. It had checked the audience. It had checked the application ID. It had looked up the user. Four validation layers, all of them working, all of them reporting the same polite errors that had been coming back for ten days. The token’s signature section was empty. Nobody had checked it.
The write-up is Faav’s blog post, published September 25, 2026. The researcher is 16, has been running bug bounties against Microsoft around school for a year, and, relevant to anyone building agents, used one to open the lead.
What actually broke
Titan’s frontend sits behind a VPN REQUIRED page, so the interface is out of reach for anyone outside Microsoft. The API is a different story. Faav’s agent, Antares, enumerated Microsoft subdomains, found an endpoint resolving to an Azure Cloud Services host, and read its public Swagger document. Four routes were listed: /GetConfiguration, /GetOnboardedTables, /v2/Query and /v2/Insert.
Three of the four required Azure AD bearer authentication. The exception was /v2/Query, the one route that accepted raw SQL.
An unauthenticated request returned 401 Unauthorized, which is the correct answer and the point at which most scans move on. What followed was a ten day grind through error messages. A token minted in the researcher’s own Entra test tenant produced a tenant error. Correct the tenant, and the error became an audience mismatch. Correct the audience, and it became an application allowlist rejection. Correct the application ID, and it became a user lookup.
Each iteration produced a slightly better error, and each iteration kept the signature byte for byte identical. The token was being parsed for its claims and never checked for its signer. Faav’s description of the tell is worth keeping: it was like a bouncer checking the name on every ID and never looking at the photo.
So he dropped the signature entirely:
{"alg":"none","typ":"JWT"}
Base64url the header, base64url the payload, end the token with a trailing period and nothing after it:
base64url(header).base64url(payload).
Titan accepted it. The remaining problem was the upn claim, which had to name a user the service recognised. Antares had been running Codex and Claude on the lead and both models spent the interval testing plausible addresses: employee-style logins, service aliases, published names. All of them failed, and the finding stayed a lead.
The pivot was not more enumeration. It was a change of question. Instead of asking what a valid UPN looks like, Faav asked what the backend might be doing with the claim:
I stopped guessing and started thinking about what the backend was actually doing with the claim. What if it was using
upnto look up a local application username?
Titan was. The claim was being passed to a local user table, and admin resolved there to local user ID 1, which carried the Admin role. The SQL ran.
The blast radius, and what is actually a fact
Here the write-up is careful, and the care matters more than the number.
Confirmed by the researcher’s own testing: 30 of the 56 archived routing values were still live, resolving through 24 configurations to 17 connected analytics databases spanning 9,863 unique table names. Bounded metadata reads returned roughly 25,000 account and email records, 17,990 employee email records, 15,001 employee organisation records, 355 database configurations, 20,979 virtual-dataset SQL definitions, 24,569 dashboards, 425,891 charts and 27,347 dataset definitions. Two one-row queries confirmed Bing search analytics were reachable, with country or state level location derived from reverse IP, and the researcher states explicitly that he did not identify anyone, link records across datasets, or build a profile.
The headline figure, 17,333,335,124,315, is a storage estimate. It comes from summing row counts across 17 ClickHouse databases through two independent metadata paths, system.tables.total_rows and active system.parts, and Faav says plainly that it likely includes historical, duplicated and derived data.
So the correct way to read 17.3 trillion is: that was the estimated size of the environment technically reachable through the bypass. It is not 17.3 trillion records about people, and it is not 17.3 trillion unique or customer records. Any coverage that drops the qualifier is overstating what was demonstrated.
Three things can be stated as observed facts. The API endpoint accepted an alg: none token with an empty signature. The upn claim was mapped to a privileged local account. Microsoft locked the endpoint on September 9, awarded a 5,000 dollar bounty on September 17, and opened case 144051 the day the report landed. There is no evidence any third party used the flaw before it was reported.
One further fact is worth carrying alongside, because it shapes how the document should be read: Faav states that Microsoft had editorial control over the post, cutting sections and figures and reshaping how the impact is described before publication. That is a normal coordinated disclosure arrangement and it is also a reason to treat this specific write-up as a jointly shaped artefact rather than a neutral one.
Four gates and a missing lock
The instructive part is the shape of the failure, not the size of the environment.
Everything in this service looked deliberate. The token was parsed. Claims were extracted. Tenant, audience and application ID were each checked against an expected value, and each check was ordered correctly and returned an accurate error when it failed. Then a user lookup ran against a local table.
That structure is what makes the defect easy to miss in review. Every gate you can see works. The reason none of them mattered is that all four operate on data the caller supplies, and the one mechanism that establishes whether the caller is entitled to supply it was absent. A JWT has three sections, and the third one is the entire security property. Parse header.payload. and you have read an unsigned assertion that anyone can author.
The second-order mistake is what the claims were used for. A upn claim arriving from an anonymous token was treated as a local username suitable for a lookup in a privileged table. That one mapping is what turned a broken signature check into an administrator session, and it is the part of the chain an application can fix independently of the token library: an identity asserted by a token should never be allowed to address a local account whose name is not in the token’s own signed namespace.
The mitigations are ordinary and they are not a blocklist. Reject alg: none. Pin the accepted algorithm list rather than trusting the header’s own choice, which is the whole point of the alg confusion class. Verify the signature before reading a single claim. Validate issuer and audience. And keep the local privilege lookup on the far side of a check that could not have run on an unauthenticated request.
The agent ground the errors, the human questioned the name
Faav’s own account of the tooling is the second half of the story and it does not say what the coverage implies.
Antares did real work. It enumerated subdomains. It found an unlinked endpoint. It recovered 56 table definitions from the Wayback Machine by reading archived Superset configuration. It walked a token through ten days of JWT error messages, one field at a time, until the unsigned token cleared four layers of validation. That is the enumeration half of vulnerability research, and it is the half that is mostly mechanical, persistent and unglamorous.
What it could not do was notice that the field name was lying. Faav’s summary is precise on this point:
What it couldn’t do was realize that
upnwasn’t actually a UPN. Looking back, theUser not founderror should’ve been the tell.
The agent’s models kept generating plausible email-formatted identities, because a UPN is normally email-formatted. The word admin is not a valid UPN, and for that exact reason it never appeared in a candidate set built from what UPNs are supposed to look like. The bug was reachable only by discarding the field’s semantics and asking what a developer might have stored in it.
That is the operator lesson for anyone wiring agents into security work, and it generalises past bug bounty. An agent optimising against error messages will climb whatever ladder the errors describe, and it will climb it for ten days without complaining. It will not, on its own, decide that the ladder is mounted on the wrong wall. Persistent error-driven loops reward the shape of the feedback, so a verifier that only checks whether the last step worked will happily certify a session that was never authenticated. The check that mattered here was semantic, not procedural. Faav’s closing line on the collaboration is the honest version of the split: Antares wouldn't have gotten here alone, and neither would I.
What to change
- Verify the signature before you read any claim, and reject unsigned tokens outright. Claims are attacker input until the signer is established.
- Pin the algorithm list server side. Do not let the token’s header choose how the token is validated.
- Never map a token-supplied identifier into a privileged local account namespace. The
upnclaim is a claim, not an authorisation grant. - Give every external endpoint the same authentication requirement, including the one route that accepts raw SQL. The exception is where the class of bug lives.
- When an agent drives discovery, require a semantic check on the happy path, not just an absence of errors. An unsigned token that reaches a user lookup reports
User not found, which reads like progress.
The uncomfortable truth in this disclosure is not that an enormous Microsoft analytics estate sat behind one endpoint. It is that four correctly implemented checks, each doing exactly what it was written to do, added up to no security at all because the fifth mechanism was skipped. Complexity is not a control. A chain of gates is only as strong as the one that establishes who is allowed to approach it, and every additional claim you validate before the signature is a claim you validated for nobody.
Sources:
- How I Could’ve Accessed 17 Trillion Microsoft Records, Faav, published September 25, 2026 (primary: the August 25 Titan discovery and the public Swagger route list, the
/v2/Queryraw SQL route with no bearer requirement, the 2023 Wayback snapshot and 56 archived table definitions, the four-stage JWT error progression with an unchanged signature, thealg: nonetoken, theupn: adminpivot to local user ID 1 and theAdminrole, the metadata counts, the 30 live routing values through 24 configurations to 17 databases and 9,863 table names, the17,333,335,124,315estimate viasystem.tables.total_rowsandsystem.parts, the explicit statement that impact is hypothetical and includes duplicate, historical and derived data, the MSRC case 144051 timeline, the 5,000 dollar bounty, and the disclosure that Microsoft had editorial control over the post) - 16-Year-Old Researcher Finds Microsoft Auth Vulnerability that Exposes 17.3 Trillion Stored Records, Cyber Security News, September 26, 2026 (secondary: independent summary confirming the Swagger route list, the four claim checks, the Bing one-row confirmation and the bounty timeline; note its headline states the 17.3 trillion figure without the storage-estimate qualifier that the primary source attaches to it)
- @The_Cyber_News on X, announcement, September 26, 2026 (discovery-layer signal: the radar hit that surfaced this story, 155 likes, 43 reposts and 7,026 views at capture; the account links the Cyber Security News write-up and repeats the unqualified 17.3 trillion framing. Verified live via the hosted X post reader)
- Faav’s blog, blog.faav.net (context: the researcher states in the primary post that this is his second Microsoft disclosure, following “Break into any Microsoft building: Leaking PII in Microsoft Guest Check-In”, published a little over a year earlier when he was 15)