Vulnerabilities
Microsoft Titan took unsigned JWTs: check your own APIs
Microsoft's internal Titan analytics API accepted alg none tokens and gave a 16-year-old admin SQL access. How the bug works and how to test your APIs.
A 16-year-old researcher who goes by Faav got administrator access to Titan, an internal Microsoft analytics service, by sending it a JSON Web Token (JWT) with no signature. Titan's API read the identity inside the token without checking that anyone had signed it. With the user field set to admin, he could run SQL against 17 connected ClickHouse databases that, by his estimate from database metadata, held about 17.3 trillion stored rows.
Microsoft locked the API down on September 9, four days after his report, and paid a $5,000 bounty. No CVE was assigned and there is nothing for Microsoft customers to patch. The same mistake is easy to make in any service that accepts JWTs, and services believed to be internal are the ones least often tested for it.
How it works
A JWT has three parts separated by dots: a header that names the signing algorithm (alg), a payload of claims such as the user and the audience, and a signature over the first two. The claims are only base64-encoded, so anyone can read or rewrite them. The signature is the one thing that proves the token came from the identity provider, and a server that skips it is trusting text the client wrote.
Titan's web interface sat behind Microsoft's VPN, but its API ran on an Azure host reachable from the internet, with a public Swagger file listing its routes. One of them, /v2/Query, accepted raw SQL. Titan did check the tenant, audience, application ID and user principal name (upn) claims. It never checked the signature.
The JWT standard defines an algorithm called none for unsecured tokens: the header says "alg":"none" and the signature section is empty. Faav sent such a token and Titan accepted it. Titan also used the upn claim, normally an email address, as a local username. Setting it to admin matched local user ID 1, which held the Admin role.
Faav's write-up says his own AI-assisted tool, Antares, found Titan on August 25 and spent about ten days probing its token checks without finding a username that worked. The admin guess was his. He reported to the Microsoft Security Response Center (MSRC) the same day it worked, September 5, as case 144051.
What was exposed, and what was not
The 17.3 trillion figure is a storage estimate summed from metadata, and Faav says it likely includes historical, duplicated and derived data. It is not a count of people or of records taken. What he did see was platform metadata: about 25,000 account and email records, 17,990 employee email records, 15,001 employee organisation records, 9,863 table names and 24,569 dashboards. He ran two one-row samples against a Bing analytics source and says he did not touch customer data or personal information.
No source reports exploitation by anyone else. MSRC asked him to stop testing and for his IP address between September 6 and 8, then closed the endpoint on September 9. Microsoft thanked him for the coordinated disclosure. His write-up went out on September 25, and he says Microsoft had editorial control over it and had some sections and figures cut or reworded.
How JWT verification fails
RFC 8725, the IETF's best current practice for JWTs, lists the two classic failures. In the first, an attacker changes alg to none and a library that trusts the header "validates" the token without checking any signature. In the second, called algorithm confusion, an attacker changes RS256 (an RSA signature checked with a public key) to HS256 (an HMAC checked with a shared secret). A library that follows the header then checks the HMAC using the RSA public key as the secret, and since that key is public, the attacker can sign anything.
Titan shows a third, quieter failure: code that decodes the token and validates claims but never calls the verify step at all. Checks on tenant and audience pass code review easily, but without a verified signature they only test values the attacker wrote.
The fix in RFC 8725 section 3.1 is that the caller, not the token, decides the algorithm. Libraries must let the application name the accepted algorithms and must not use any other, and each key must be used with exactly one algorithm.
What to do
Pin the algorithm in code
- Node.js
jsonwebtoken: passalgorithms: ['RS256'](or whatever you issue) tojwt.verify(). Versions up to 8.5.1 could accept unsigned tokens when no algorithm was given and the key was falsy (CVE-2022-23540); version 9.0.0 removednonefrom the defaults. Upgrade, and pin anyway. - Python PyJWT: always pass
algorithms=["RS256"]tojwt.decode(). Its documentation says to hard-code the list or configure it next to the key, never to build it from the token's ownalgheader, and not to mix HS and RS algorithms for the same key. josefor JavaScript:jwtVerifynever accepts unsecurednonetokens. Itsalgorithmsoption still defaults to every algorithm that fits the key, so set it explicitly.- Search your code for decode calls that skip verification, such as
jwt.decode()injsonwebtoken(which never verifies) oroptions={"verify_signature": False}in PyJWT, and check none of them feed an authorisation decision.
Test your own APIs
Take a valid token for a low-privilege test user and replay it in three forms against every authenticated route: with the header changed to {"alg":"none"} and the signature removed; with the signature bytes altered; and with a privileged claim such as the user, role or upn edited. Each must return 401. For RS256 services, also try an HS256 token signed with your published public key. Burp Suite's JWT Editor extension and jwt_tool automate these cases.
Then check how claims map to accounts. If a username or upn claim is looked up in a local user table, confirm that a value like admin or root cannot land on a built-in account, and that user ID 1 is not a superuser reachable by name.
Look for past abuse
In your API gateway or application logs, search for requests whose Authorization header token has a header that decodes to alg of none (the base64 prefix eyJhbGciOiJub25lI covers the common spelling), and for successful requests with tokens your identity provider never issued, which you can spot by a token ID (jti) or issued-at time missing from its sign-in logs. If you find any, treat every session and API key reachable through that service as exposed and rotate them.
Test internal APIs as if they were public
Titan's interface was VPN-only, and that likely shaped how much scrutiny the API got. The API host itself was on the internet, documented by its own Swagger file, with a route that ran raw SQL. List the hosts behind your internal tools from DNS and certificate transparency logs, and test each one as if it were public.
Our web penetration tests run the token tampering cases above against every authenticated route, and our attack surface management work finds the API hosts that were meant to stay internal. Open the chat and Yaali, our AI agent, will pass your question to the engineer who would do the work.
Sources: Faav's write-up, Help Net Security, Rescana, IT-Connect, Hackread, Security Magazine, Runtime Wire, RFC 8725, jsonwebtoken advisory GHSA-qwph-4952-7xr6, NVD CVE-2022-23540, PyJWT API reference, jose VerifyOptions.