One Slash to Root
Dissecting the TeamCity authentication bypass that handed attackers the keys to the build kingdom.
Build servers are the soft underbelly of modern engineering. They have credentials to your registries, your cloud, your code. They run untrusted input as a job description. And yet they get a fraction of the scrutiny we apply to the apps they build.
The setup
JetBrains TeamCity is a popular self-hosted CI/CD server. Like every CI server, it exposes a web UI for humans and a REST API for automation. The REST endpoints can do extremely sensitive things: create administrator accounts, grant API tokens, modify build configurations that already have access to your secrets.
So naturally, those endpoints require authentication. Or rather: a request handler ahead of them is supposed to check whether the request is authenticated, and reject it if not.
The bug
The vulnerability lives in how TeamCity decides which requests need authentication checks. The router applies authentication to URLs that match its API patterns. Static-resource paths get a free pass.
It turned out the router could be tricked into classifying an authenticated endpoint as a static resource by appending a particular suffix that didn't actually change which controller would ultimately handle the request:
POST /res/.;.jsp?jsp=/app/rest/users/id:1/tokens/x HTTP/1.1
Host: target:8111
Content-Length: 0
The /res/.;.jsp prefix made the auth filter shrug and let the request through. Then the inner routing took the ?jsp= hint, dispatched to the real REST endpoint, and created an API token for user id:1 — the built-in admin account.
One unauthenticated POST. One admin token. Done.
Anatomy of the bypass
This is a classic parser differential bug. Two pieces of code looked at the same URL and disagreed about what it meant:
- The auth filter saw a static resource path. "Not my problem." Pass.
- The servlet dispatcher saw an API call. "I can handle that." Execute.
Whenever two parsers in a pipeline interpret input differently, you have an attack surface. Path traversal sequences, semicolon parameters, double-encoding, alternate slash characters, fragments — all are tools for creating disagreement.
Why CI compromise is catastrophic
An admin token on a build server is not a "we lost some data" incident. It's a supply-chain incident:
- Modify any build configuration to inject code into the next release artifact.
- Exfiltrate every secret stored in the server (cloud creds, signing keys, deploy tokens).
- Pivot to every system the runners can reach (often production).
Within days of disclosure, multiple ransomware crews and at least one state-aligned group were spraying the exploit at exposed instances. Researchers found tens of thousands of internet-reachable TeamCity servers in the first week.
The fix, and the principle
JetBrains shipped a patch that normalized URLs before authentication decisions, so the two parsers agreed again. The general principle worth internalizing:
If your authentication decisions depend on string matching against a URL, you are one parser quirk away from a 9.8 CVSS.
Authenticate by handler identity, not by URL pattern. Whatever code is about to run should declare its own auth requirements, and the framework should refuse to dispatch to it without checking. Anything else is a future blog post.