The Gateway Holds the Keys: One Researcher’s 11 October Batch Reads Credentials Out of Seven Agent Platforms

Nobody needed to attack a model. On 11 October 2026, between 14:17:03Z and 14:17:06Z, NVD published a batch of advisories assigned by VulnCheck and credited, every one of them, to a single researcher: hieuPenguinnn. We covered the two strongest separately — hyper-mcp’s pair of cosign bypasses. This is the rest, and the rest has a theme.

Seven of these findings end in the same place: an attacker reads a credential belonging to someone else out of the agent platform’s own infrastructure. Not out of a prompt, not out of a model’s weights, not out of a tool result. Out of a config file, an admin port, a log line, or an authorization table. The LLM is not a participant in any of them.

Three places a key goes, and all three leak

Sorted by mechanism rather than by score, the batch resolves into three classes. The first is the oldest bug in software, wearing a new logo.

CVE-2026-108860 — BotSharp, 9.3 critical, CWE-321, CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N. BotSharp is a .NET framework for building LLM agents, 3,111 stars and 650 forks. We read src/WebStarter/appsettings.json at the commit the advisory pins, and the JWT block is four lines of plaintext:

"Jwt": {
  "Issuer": "botsharp",
  "Audience": "botsharp",
  "Key": "31ba6052aa6f4569901facc3a41fcb4adfd9b46dd00c40af8a753fbdc2b89869"
},

A 64-character hex HMAC secret, in the repository, alongside a fixed issuer and a fixed audience. In BotSharpOpenApiExtensions.cs that value is read straight into the validation parameters:

ValidIssuer = config["Jwt:Issuer"],
ValidAudience = config["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"])),

Every deployment that starts from the shipped starter project and never rotates that key accepts bearer tokens signed by anyone who has read the repository. Issuer and audience are not secrets and are not deployment-specific, so there is nothing left to guess: an attacker mints a token for any known username, including an administrator, and presents it to [Authorize]-protected routes. VulnCheck’s description says exactly this, and the code agrees.

One detail we noticed that the advisory does not mention, and we raise it as an observation rather than a second finding. The same function takes an enableValidation boolean, and when it is false the code installs a replacement signature validator:

o.TokenValidationParameters.SignatureValidator = (string token, TokenValidationParameters parameters) =>
    new JsonWebToken(token);

That callback parses the token and returns it. It does not verify anything. In that mode the hard-coded key is irrelevant because no signature is checked at all — an unsigned token is accepted. Whether any deployment runs that way depends on the caller, which we did not trace through every host project, so we make no claim about prevalence. We note only that the committed key is the better of the two configurations.

An OAuth server with nowhere to record the user

CVE-2026-108865 — AmoyLab Unla, 8.8 high, CWE-287, CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N. Unla is an MCP gateway: it fronts MCP servers behind a prefix, applies auth, and proxies to upstream APIs with injected credentials. 2,240 stars. The advisory says its OAuth2 server “never authenticates a resource owner.” That phrasing undersells it.

We read Authorize in internal/auth/oauth.go at v0.10.0 end to end. It reads client_id, redirect_uri, response_type, scope, state and the PKCE parameters from the query string. It rejects a missing client ID, redirect URI or response type. It rejects a response type that is not code. It looks up the client and rejects an unregistered one. It validates the redirect URI against the client’s registered list, and validates the PKCE method if a challenge was supplied. Then it generates an authorization code and returns it.

Every check in that list is a check on the client. At no point is a human asked to log in, and at no point is a session consulted. The decisive evidence is not in the handler, though — it is in the data model. Here is the record the handler persists, from internal/auth/storage/store.go:

type AuthorizationCode struct {
	Code                string   `json:"code"`
	ClientID            string   `json:"client_id"`
	RedirectURI         string   `json:"redirect_uri"`
	Scope               []string `json:"scope"`
	CodeChallenge       string   `json:"code_challenge,omitempty"`
	CodeChallengeMethod string   `json:"code_challenge_method,omitempty"`
	ExpiresAt           int64    `json:"expires_at"`
	CreatedAt           int64    `json:"created_at"`
}

There is no user field. No subject, no owner, no principal. The struct has no place to put the answer to “who authorised this?”, which means the question was never asked anywhere upstream of it. This is not a missing check that a reviewer skimmed past; it is a type that cannot express the thing OAuth exists to express. An authorization code grant where the code is unbound to a resource owner is a key-issuing endpoint.

The exploitation path follows from the MCP specification’s own requirements. Dynamic client registration means an attacker registers a client of their own, requests a code at /authorize, exchanges it at /token, and holds a token that handleRoot will accept — we read the gate, and it does nothing more than call isValidAccessToken when the prefix is configured for OAuth2. What that token unlocks is the gateway’s whole purpose: OAuth2-protected MCP prefixes, proxied upstream APIs, and the credentials Unla injects on the way out. The attacker never sees those credentials, and does not need to. They get their use.

The admin port that was never meant to be reachable

CVE-2026-108863 — Katanemo Plano, 8.7 high, CWE-306, CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N. Plano is an Envoy-based LLM gateway with 7,084 stars — the most-starred project in the batch. The entire defect is the first three lines of config/envoy.template.yaml:

admin:
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }

Envoy’s admin interface has no authentication by design; the upstream documentation is explicit that it must not be exposed. Bound to 0.0.0.0, port 9901 answers to anything that can route to the container. /config_dump returns the running configuration, and because Plano implements its routing logic as a WASM filter whose configuration carries the LLM provider credentials, the dump contains those API keys in plaintext.

This one is worth separating from the others because the bug is not in Plano’s code at all. It is a default in a template, and the capability it exposes is a property of a dependency behaving exactly as documented. There is nothing to exploit — curl is the exploit. It is also the easiest to mitigate without a patch: bind the admin socket to 127.0.0.1 and stop publishing 9901.

CVE-2026-108862 — APIPark, 6.0 medium, CWE-639, 1,818 stars. The same outcome through the ordinary application path. Authenticated users holding authorization-view permission on one application can supply another application’s authorization UUID and receive its plaintext API keys. We read module/application-authorization/iml.go at v1.9.7-beta: Info validates the service it was asked about, fetches the authorization by UUID, unmarshals the stored config, and returns it — with no check that the authorization belongs to the service. The HideCredential flag is returned as a field of the response rather than applied to it, which is the whole bug in miniature: a display preference where an access control should be.

Secrets in the telemetry

The third class is the quietest, and in our view the most under-rated of the batch, because the credential is not exposed to the network at all — it is written down.

CVE-2026-108857 — Hugging Face Text Embeddings Inference, 4.8 medium, CWE-532, 5,077 stars. TEI accepts an --api-key to require a bearer token on its embedding and rerank endpoints. The argument is declared in the router’s Args struct with no redaction attribute:

/// Set an api key for request authorization.
#[clap(long, env)]
api_key: Option<String>,

And main logs the parsed struct wholesale, immediately after initialising telemetry:

tracing::info!("{args:?}");

Derived Debug prints every field. The key that protects the service is written to the router’s first log line at info level on every start. Anyone with container output, a log aggregator, or — because init_logging is wired to otlp_endpoint on the line above — an OTLP trace backend, recovers it. 4.8 is a fair score for the direct impact and understates the blast radius, since logs and traces routinely cross trust boundaries that the service itself never would.

CVE-2026-108858 — Predibase LoRAX, 6.8 medium, CWE-532, 3,837 stars. The same class, worse provenance. LoRAX accepts a per-request api_token in the body of POST /generate so callers can load private adapters, and that field is part of an instrumented span, so it reaches router logs and OTLP backends. The difference from TEI is whose secret it is: TEI logs the operator’s own key, LoRAX logs its users’ tokens. An operator reading their own logs is reading other people’s credentials for third-party model repositories. The repository was last pushed 28 May 2026, roughly four and a half months before disclosure.

The ACL that did not follow the data into the tool

Two findings in the batch invert the pattern, and they are the ones with the clearest agent-security content. Both are MCP interfaces bolted onto mature applications that already had working permission models.

CVE-2026-108861 — Odoo MCP 1.0.0 through 1.3.2, 5.3 medium, CWE-200. The server enforces a field-level ACL on its purpose-built tools, and also exposes a generic execute_method tool. Calling read or search_read through execute_method and naming denied fields returns them unredacted. The advisory explicitly names “prompt-injected agents” alongside human attackers — a CNA writing injection into the threat model of a data-access CVE, which remains rare enough to be worth noting.

CVE-2026-108851 — phpMyFAQ through 4.1.10, 4.8 medium, CWE-862. The MCP server’s faq_search tool reaches Search::searchDatabase(), which never applies the user or group permission checks the web application applies. Any client connected to the MCP server retrieves the full text of FAQs restricted to specific users or groups.

Neither is an AI vulnerability in any interesting sense. Both are the consequence of adding a second front door to an application whose authorisation logic lived in the first one. We made the same argument at greater length about JeecgBoot’s AI module, where twenty-two of seventy-six missing-authorization CVEs landed in the newest feature set. The pattern is now well enough attested to state plainly: when a mature product grows an MCP surface, the permission model does not come along by default, and the newest code reliably has the thinnest coverage.

And one in the transport

CVE-2026-108859 — mcp-go through 1.2.1, 8.7 high, CWE-770, CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N. This one does not fit the credential theme but is the highest-reach item in the batch by a wide margin: mark3labs/mcp-go has 9,155 stars and 901 forks and is the dominant Go implementation of MCP. The defect is three lines in StreamableHTTPServer.ServeHTTP:

var body []byte
var bodyErr error
if r.Method == http.MethodPost && r.Body != nil {
	body, bodyErr = io.ReadAll(r.Body)
}

No http.MaxBytesReader, no length check, and it happens before any validation — including before authentication, since the read is at the top of the handler. An unauthenticated client sends a very large or merely numerous concurrent POST bodies and the process is OOM-killed. The comment above those lines explains the design reason honestly: the body is read up front so the transport-agnostic core never holds an io.Reader across an SSE upgrade. That is a reasonable architectural choice that happens to require a bound nobody added.

The timing is the part to sit with. v1.2.1 was released on 9 October 2026 at 09:28:16Z, and the advisory’s affected range is “through 1.2.1” — the CVE describes the newest release, published two days earlier. Every Go MCP server built on the streamable HTTP transport inherits this, and almost none of them will learn about it from their own advisory feed, because the finding is against a library they depend on rather than against them. We have made this point about this SDK before, and it has not become less true.

What nobody can patch to

We checked the release history of every repository in this batch against each advisory’s affected ceiling. The result is uniform, and it is the finding we would lead with if we had to pick one:

  • BotSharp — newest release r5.2-image-composition, 17 October 2025. Properly a year old; the advisory covers “through 5.2.0”.
  • Unla — newest release v0.10.0, 4 August 2026. The advisory covers through v0.10.0. Repository last pushed 27 August 2026.
  • Plano — newest release 0.4.37, 28 September 2026. The advisory covers through 0.4.37.
  • mcp-go — newest release v1.2.1, 9 October 2026. The advisory covers through v1.2.1.
  • APIPark, TEI, LoRAX, Odoo MCP, phpMyFAQ — affected ranges likewise terminate at the current version.

Not one of these advisories has a version to upgrade to. That is thirteen records published on a single day against actively maintained projects with a combined thirty-odd thousand stars, and the remediation advice available to every reader of every one of them is “wait.” We have now written this paragraph about PraisonAI, openPDC and Argo CD within a fortnight, and the mechanism is consistent: a third-party CNA with a disclosure calendar meets a maintainer without one.

The compensating detail is that most of this batch is configuration-reachable. Bind Envoy’s admin socket to loopback. Rotate Jwt:Key and change the issuer and audience away from botsharp. Put a MaxBytesReader or a reverse-proxy body limit in front of an mcp-go server. Pass TEI’s key through an environment variable and keep the router’s logs out of shared telemetry. Do not front an MCP server with Unla’s OAuth2 mode at all until the authorization code learns who a user is — and that one, uniquely, cannot be configured around, because the fix is a schema change.

The shape of it

Thirteen advisories, one researcher, one afternoon, and the model never comes up. What these systems have in common is not that they run inference; it is that they sit between a caller and a credential, which is the definition of a gateway. Every finding here is a failure of the gateway to keep the two apart — through a committed key, an unbound token, an open admin port, a missing ownership check, a log line, or a tool that outranks its own application’s ACL.

The infrastructure built to stop agents leaking secrets is, at the moment, the most reliable place to find them. That is not a prompt injection problem and no amount of model-layer defence addresses it. It is 2010-era web application security, applied late, to a layer that accumulated credentials faster than it accumulated review.

Verification note. All CVE identifiers, CVSS 4.0 base scores and vectors, CWE assignments, publication timestamps (14:17:03Z–14:17:06Z on 11 October 2026), NVD status values and the assigning source (disclosure@vulncheck.com) were read first-hand from the NVD 2.0 API. Severity labels, advisory dates and the credit to hieuPenguinnn were read from the individual VulnCheck advisory pages; we confirmed the same credit on all thirteen. Every source excerpt in this briefing — BotSharp’s appsettings.json and BotSharpOpenApiExtensions.cs, Unla’s Authorize handler, the AuthorizationCode struct and the handleRoot auth gate, Plano’s envoy.template.yaml, APIPark’s iml.go, TEI’s Args struct and main, and mcp-go’s ServeHTTP — was read directly from the referenced repository at the tag or commit the advisory pins, and is quoted verbatim. Release tags and dates, star and fork counts and last-push timestamps came from the GitHub API and were checked against each affected range individually. The three-class grouping, the observation about BotSharp’s alternate signature-validator path, the reading of Unla’s struct as structural rather than an omitted check, the comparison of TEI and LoRAX by whose credential is logged, and the closing argument are our editorial analysis, not vendor or advisory statements. We found no evidence of exploitation of any of these and do not imply any. No request was sent to any third-party instance of any product named here, and no proof-of-concept was executed.

Sources: