How a gateway leaks the keys it holds
Four ways an LLM gateway hands out the keys it stores, the October 2026 LiteLLM CVEs behind each one, and the kRouter releases that fixed the same shapes.
One sentence from Check Point Research's 13 October 2026 write-up on LiteLLM explains why a gateway is a different kind of target from the services behind it:
The attacker's server receives the full request, including any configured API keys in the proxy's default headers.
That is CVE-2026-59823. A caller sends a request body naming the host it wants the proxy to talk to, and the proxy obliges — attaching, as it always does, the provider credential it stores on your behalf. Not a data leak, not quota theft. The key itself, delivered.
This post is about the four shapes that produce that outcome. All four are generic: any gateway with an admin endpoint, a route allowlist, a caller-supplied URL or a published quickstart credential has somewhere to put each one. kRouter has shipped three of the four and fixed them, with version numbers, and those are in here too.
What a gateway is actually holding
A metered API key is replaceable: notice the spend, rotate it in a dashboard, move on. A gateway that fronts subscription tools holds something worse — OAuth tokens minted by a sign-in flow for Claude Code, Codex, Copilot, Antigravity and Kiro. There is no rotate button for those. You re-authenticate, you may trip a device-trust check, and in the worst case the provider flags the account.
As of 19 October 2026 there are three BerriAI LiteLLM entries in CISA's Known Exploited Vulnerabilities catalog, added 8 May, 8 June and 2 September 2026. KEV listing means observed exploitation, not theory; Wiz's September report says its honeypots first saw the MCP authentication bypass used on 7 July 2026. None of the three is there for a clever cryptographic break. All three are ordinary web-application bugs in software that happens to hold everyone's keys.
Shape 1: a route allowed by its prefix
LiteLLM's POST /config/update checked that the caller was authenticated but not that they were an admin. The route classifier used a startswith test against a list that contained /config, so /config/update matched and any ordinary dashboard session token slid through. The GitHub advisory for CVE-2026-35029 lists the consequences: modify configuration and environment variables, register pass-through handlers pointing at attacker-controlled Python, read arbitrary files, and overwrite the dashboard username and password.
kRouter shipped the same class of bug in the opposite direction.
kRouter's Next config rewrites /codex/:path* onto its Responses endpoint, but /codex was absent from the guard's list of public prefixes. The guard runs before rewrites, so it saw a literal path it had no branch for — not the public-API branch, not the deny-by-default branch for /api/*, not the dashboard branch — and let it through unauthenticated. From off the machine with no credentials, POST /v1/responses returned 401 while POST /codex/responses returned a real streaming completion. Being absent from the allowlist is what broke it, because that list is what routes a path into the check that demands a key. Fixed in 0.5.149.
The lesson is not "audit your allowlist". It is that an allowlist and a router are two descriptions of the same URL space, maintained separately, and they drift. kRouter's fix was a test that asserts the invariant rather than the one path: every root-level rewrite source must have its top-level prefix guarded.
Shape 2: a URL from the request, and your key goes with it
CVE-2026-59823 is the key-leak one. LiteLLM did filter request bodies — it blocked api_base and base_url. The filter inspected two top-level keys, and the user_config object, from which the proxy builds a router for that request, was not one of them. The same api_base nested one level down was never looked at.
Two details the headlines dropped. The advisory rates it medium, not critical, and says exploitation requires a valid virtual key — so the threat model is a caller you already issued a key to, not an anonymous stranger. And GitHub's advisory database puts the fix in 1.83.9 for this one but 1.83.0 for the config bug, five months earlier; Check Point's summary line that both are resolved as of 1.83.9 is true but reads as if they shipped together.
kRouter's version was /v1/search, where a client-supplied base URL was returned verbatim and handed to eleven outbound request builders. Fixed in 0.5.136. The subtler instance is still live by design and worth describing, because it is the same shape without an attacker: the async image providers submit a job, read a status URL out of the provider's own response, and poll it — with the provider's credential in the headers. If that response can name a host, the key goes there. Those polls now run through a reader that resolves the hostname and re-validates every redirect hop rather than following it.
Shape 3: a credential your own documentation published
Quickstart snippets get pasted whole. kRouter's docs, and upstream 9router's, showed example values for the dashboard password and the session signing secret, and kRouter used them verbatim. Anyone who pasted the website's signing key had a key the internet knows and a stranger could forge a session with it.
The fix, in 0.5.162, is unglamorous: a list of the exact strings the documentation ever printed. A signing secret that matches one of them, or that is shorter than 32 characters, is ignored in favour of the random one kRouter generates in its data directory, with one warning in the log. A first-login password that matches one gets the built-in default's rule.
That rule is the part worth copying. The default password is not forbidden; it is made useless at a distance. A login with it from the machine kRouter runs on works, and from anywhere else returns 403 with no cookie set. An earlier version computed the same condition but set the cookie first and returned "you should change this" as a flag the caller could ignore — then use the session to turn authentication off. Order matters more than the check.
Under Docker your own browser is never on the machine, so the documented run command generates a password rather than printing one:
KROUTER_PASSWORD="$(openssl rand -base64 18)" && echo "Dashboard password: $KROUTER_PASSWORD"
docker run -d \
-p 127.0.0.1:20128:20128 \
-e INITIAL_PASSWORD="${KROUTER_PASSWORD:?run the line above first}" \
-v "$HOME/.krouter:/app/data" \
--name krouter \
sifxprime/krouter:latestShape 4: a gate that trusts a value the caller chose
LiteLLM's KEV-listed MCP bypass, CVE-2026-59822, is this shape: when key validation failed, the fallback path replaced the failure with an empty auth object, so a fabricated Authorization header reached the MCP tooling. The caller supplied the thing that decided the outcome.
kRouter's was the Host header. Its "is this caller on loopback?" test read a value the client chooses, so any remote request sending Host: localhost counted as local — which grants key-free API access and exemption from the login rate limiter. Fixed in 0.5.135 by deriving the peer address from the TCP socket, stamped with a per-process secret the client cannot know or replay, and failing closed in production when that stamp is absent rather than falling back to the header.
Both are the same question: when your authorisation decision reads an input, ask who writes it.
What does not work
- A deny-list of bad address strings. kRouter's SSRF guard was behind upstream's and had seven live bypasses: IPv4-mapped IPv6 in three spellings, NAT64, the deprecated IPv4-compatible form, and trailing-dot hostnames for both
localhost.and the Google metadata name. The guard now parses an address into numeric groups and reasons about the value, because::ffff:127.0.0.1and::ffff:7f00:1are the same address and a string match is a coin flip on which one the URL parser hands you. - Checking only the first hop. A URL that passes the check can redirect into loopback. Validate each hop, or do not follow redirects.
- Checking only literal addresses.
127.0.0.1.nip.iopasses every literal test and resolves to loopback. kRouter's test suite pins exactly that gap: the synchronous check allows it, the resolving one rejects it. - Putting the check in each call site. This is the one that actually bit. The first attempt swapped in the hardened guard and updated no callers, which read as "SSRF fixed" while every hole stayed open.
- "It is behind a firewall." kRouter binds
0.0.0.0by default and says so at startup. Assume the port is reachable until you have checked from another machine.
What does: one reader, and a test that pins it
kRouter exposes three entry points from one file: a cheap literal check for rejecting obvious input at request-build time, a resolving check that adds DNS, and a fetch wrapper that re-validates every redirect. The tests do something more useful than exercising the guard — they read each call site's source and assert that it imports the right one and keeps no bare request of its own. A new image adapter that polls a provider-supplied URL by hand fails that test instead of shipping the hole again.
| Shape | Where to look in your own gateway | What catches a regression |
|---|---|---|
| Prefix-matched allowlist | Every rewrite, alias and proxy pass next to the auth allowlist | A test that derives the route list from the router config |
| Caller-supplied URL | Any request or response field that becomes a base URL, image URL or callback | A test that fails any outbound call not routed through the guard |
| Published credential | Every secret your own quickstart prints | Reject the literal strings; require a length floor on signing keys |
| Caller-chosen trust signal | Host, X-Forwarded-For, any header that decides "local" or "admin" | Derive from the socket; fail closed when you cannot prove the peer |
Then check from another machine, which is the only test that counts:
curl -i https://your-router.example.com/v1/chat/completions
# expect 401, not a completion
curl -i -X POST https://your-router.example.com/api/auth/login \
-H 'Content-Type: application/json' -d '{"password":"123456"}'
# expect 403 and no Set-CookieWhere kRouter is not the answer
It has no roles. There is one dashboard identity. LiteLLM's privilege-escalation class — a low-privilege user reaching an admin endpoint — cannot happen here because there is no low-privilege user. That also means there is no least-privilege tier to fall back on: whoever has the login has everything.
Credentials are not encrypted at rest. OAuth tokens and API keys sit in the SQLite database in the data directory. Anyone who can read that file, or a backup of it, can use your accounts. Treat it like ~/.ssh.
Provider base URLs you configure yourself are trusted and bypass the SSRF guard entirely, which is how a local Ollama works at all. That makes dashboard access, not the guard, the thing protecting you there.
Request bodies are stored by default. Observability keeps the latest records, prompts included, in the same database. Turn it off in Settings if that is not acceptable.
And this took several releases. 0.5.135, 0.5.136, 0.5.149, 0.5.162 and 0.5.166 each closed part of it, because every new feature that fetched something added a call site. The guard was the easy part.
Common questions
Am I affected if I run LiteLLM? Check your version against the advisories, not the blog summaries. As of 19 October 2026 GitHub's database gives 1.83.0 for the config endpoint and the password-hash issues, 1.83.7 for the SQL injection and the MCP command execution, 1.82.0 for the guardrails bypass, 1.83.9 for the user_config SSRF and 1.84.0 for the MCP authentication bypass. Upgrading past the highest of those covers all six.
Do these have CVSS scores? Two of them do: 9.8 for the SQL injection and 8.8 for the MCP command execution. The October pair, the MCP authentication bypass and the password-hash advisory carry no score in GitHub's database. Severity labels are there; invented numbers are not.
Is self-hosting worse than a managed gateway? Different, not worse. Self-hosting means you own patching and exposure. A managed gateway means someone else owns them and you find out when they publish. The KEV entries are for software that both kinds of operator run.
Does any of this need an attacker on the internet? The user_config SSRF needs a valid key, so it is an insider or a leaked key. The SQL injection did not: a crafted Authorization header on any LLM route reached it unauthenticated. kRouter's /codex bypass did not either. "Only authenticated users can do it" is a real mitigation and a thin one.
What is the single highest-value check? Run the two curl commands above from a different network. Most gateway exposure is not an exotic bug; it is a port that answers.
Related
- The security checklist for self-hosting an AI router — the deployment side: binding, keys, tunnels, what is listening
- LiteLLM alternative: kRouter compared — what the two actually do differently
- Best self-hosted LLM gateways in 2026 — picking one, including who can audit the code
- Inside a kRouter request — where the access check sits in the request path
- Self-hosting an AI router on a VPS — the exposed case, step by step
Published by Kodelyth, the team that builds kRouter. Posts are drafted with AI assistance and reviewed by a person before they go out. kRouter is free and MIT licensed.
Install kRouterRelated posts
- How kRouter worksInside a kRouter request: Claude Code and Codex tracedOne Claude Code request and one Codex request, followed through kRouter: key check, model and combo, account, translation, RTK, the call and fallback.
- How kRouter worksWhat is an LLM gateway, and do you actually need one?Gateway, proxy, router, aggregator: four words for overlapping things. What each one does, the problem they solve, and when not to run one.
- How kRouter workskRouter is a multimodal AI gateway, not just an LLM routerOne local endpoint for chat, speech, transcription, images, video, embeddings, web search and fetch across 95+ providers -- and where each one stops.
Relevant docs
- Core conceptsProviders, combos, account routing, token savers, MITM mode, quota tracking, the response cache and remote access: the eight ideas behind kRouter.
- The Zenith Routing EngineHow kRouter picks which of your accounts serves a request: Zenith scoring, conversation stickiness, cooldowns, and the round-robin, P2C and random options.
- Token savers: RTK, Caveman, Ponytail, Headroom, PXPIPEFive ways kRouter can shrink a request before it reaches the provider. Only RTK is on by default. What each saver does, what it needs, and its limits.