CVE-2026-111999 — Vanus: Unauthenticated CloudEvents Publish on Port 8081
Unauthenticated CloudEvents publish on Vanus gateway port 8081.
- #Vanus
- #Missing Auth
- #CWE-306
- #CloudEvents
Another finding from the same Vanus audit (`vanus-labs/vanus` @ `main@1334a7b`, clean `go build ./...` under Go 1.24.2). This one is my favorite of the package — not because the exploit is dramatic (it's a `curl`), but because of *how* I noticed it: I didn't actually find it by reading code. I found it by **drawing a picture of the ports** before reading almost anything.
§The architectural tell
Before diving into function bodies, one of my habits is mapping the network face: which ports answer the world, and what each one is supposed to be trusted to do. Rough sketch for Vanus's gateway:
- **:8080** — a gRPC proxy. Its methods pass through auth interceptors (`authPublish` → `Authorize(EventbusWrite)`, wired at `proxy.go:578–591`). This is where authentication *lives* in Vanus.
- **:8081** — an HTTP CloudEvents receiver. This is the protocol *users* are actually handed — it's in every quickstart, and the shipped Service definitions expose it right alongside 8080 (`deploy/vanus.yaml:202–213`, with no `auth:` stanza anywhere in the deploy tree).
Now the tell. Look at what :8081 does: it *publishes events*. Those are writes — the same writes the :8080 gRPC path dutifully authenticates.
But the HTTP path **never traverses any interceptor at all**, because it isn't gRPC. It calls the publish function as a plain Go method.
When I first drew that sketch, I remember thinking something to the effect of: "surely the receive handler also token-checks." Then I read `receive()` and, well.
§The code path, traced hop by hop
**Hop 1 — receiver construction, no auth.** `server/gateway/gateway.go:86–104`:
c, err := client.NewHTTP(cehttp.WithListener(ls), cehttp.WithRequestDataAtContextMiddleware())
// no auth option — compare proxy.go:578-591, which adds grpc_auth interceptors
go func() {
if err := c.StartReceiver(ctx, ga.receive); err != nil { … }
}()The bug here is famously the *absence* — no auth option on the receiver. The absence is comment-free too; nobody wrote "auth would be nice here." Every auth aid Vanus has exists *on the other listener*.
**Hop 2 — the handler.** `gateway.go:106–129` — `receive(ctx, event)` parses the path (`getEventbusFromPath`, which carries its own little `// TODO validate` notice in the code, and still supports the legacy `/gateway/{name}` form defaulting to `namespace=default`), converts the event, then calls:
_, err = ga.proxySrv.Publish(ctx, &proxypb.PublishRequest{…}) // ← DIRECT CALLNo token check anywhere in `receive`.
**Hop 3 — the publish function trusts its caller.** `server/gateway/proxy/proxy.go:166–235` — `Publish()` performs no authentication; auth lives only in the interceptor chain, which this HTTP path never passes through. So a straight Go function call from an internet-facing HTTP handler = unauthenticated write to any eventbus whose `namespace/eventbus` name you know. And `default/default` exists on every fresh install, so "any eventbus whose name you know" starts with "yes, that one."
§Proving it — in-module PoC
I wanted a PoC that runs the *actual* machinery rather than a re-implementation, so `poc-authbypass-verify` starts a genuine `cloudevents/sdk-go` HTTP receiver with the exact same options as `gateway.go:92`, then delivers an event with **no `Authorization` header at all**. Here's the evidence log, unedited:
[receiver] got unauthenticated event id=poc-evil-001 type=poc.injection source=poc-attacker (NO token checked!)
sender result: 200:
VERDICT: AUTH BYPASS CONFIRMED
Code path: startCloudEventsReceiver (gateway.go:86) -> receive (gateway.go:106)
-> proxySrv.Publish (proxy.go:166) direct call,
bypassing gRPC auth interceptors (proxy.go:578-591).Against a deployed instance, the audit needs nothing but `curl`:
curl -v http://<gateway>:8081/namespaces/default/eventbus/default/events \
-H "Ce-Specversion: 1.0" -H "Ce-Id: evil-1" -H "Ce-Source: attacker" \
-H "Ce-Type: evil.inject" -H "Content-Type: application/json" \
-d '{"forged":true}'
# => 200 ACK, event storedAnd one more gem I found while writing the submission — because of course I read the whole `Publish` function out of paranoia after finding one bug in it — sending `Ce-Xvanusdeliverytime: <RFC3339>` reroutes processing into the `__Timer_RS` system eventbus (`proxy.go:222–226`). So a single unauthenticated header also steers forged payloads through Vanus's own deferred-delivery machinery. That detail made me laugh, out loud, alone in my dev directory.
§Impact, honestly scoped
The CNA scored this as integrity-centric (`C:N/I:H/A:N` v3.1 / 8.7 v4.0), with SSVC **Exploitation=none · Automatable=yes · Technical Impact=partial**. Realistic harms, as I'd rate them for a plain ingress-exposed deployment:
- **Event forgery at scale** — anyone who can reach :8081 writes to any bus. Forged events match subscription filters and get pushed to downstream victim sinks.
- **Resource exhaustion** — writes flow into unbounded segment appends, no rate limit on this path. A flood loop, or a timer-header flood, grows durable storage indefinitely.
- **Durable poisoning** — forged data persists in segments and etcd and survives bus restarts. Dead-letter queues fill with attacker content.
- **Chain enabler** — this path is the pre-auth *write* primitive the audit's SSRF finding and CVE-2026-112000 chains relied on: publish the triggering event *and* optionally define its destination, without ever being credentialed.
§Submission and assignment
I submitted this as its own VulnCheck file (finding #2 of the Vanus package): the receiver/handler/`Publish()` taint trace, the in-module PoC transcript, the curl repro, the timer-abuse header, and concrete fixes — Bearer middleware on the CloudEvents HTTP receiver using the *same* `Authentication.Authenticate` used by gRPC, `Authorize(EventbusWrite)` enforced inside `receive` for the resolved eventbus (not only via interceptors), and dropping or gating the legacy `/gateway/{name}` path. Plus the operator basics: :8081 does not belong on an ingress. Ever.
Result:
- **CVE-2026-111999** assigned, CNA VulnCheck, published 2026-10-18
- Affected: Vanus 0.8.0 – 0.9.0 (`pkg:golang/github.com/vanus-labs/vanus`)
- Tracked under issue #647 in vanus-labs/vanus; technical ref `gateway/gateway.go#L81–L124`
- Finder credit recorded as **MJ DHURGESH**
§The lesson (or: routing-boundary bugs wearing auth-bug clothes)
This is an architecture bug, not an auth bug, though it wears an auth-bug costume:
Projects often put authentication in exactly one layer — the interceptor chain of their main RPC framework — and then add a second endpoint (a webhook, a protocol adapter, a receiver, a proxy) that calls the *same business logic* without the framework in between. Nothing "fails"; every function behaves as written. The bug is invisible in any single function's code, which is why it survived in a real product.
The defense is architectural, not charming: **any code path that can mutate state must authenticate as its own first action**, or the second entry point is always going to quietly bypass whatever the first one checks. If you can't draw your app's ingress layer without seeing two faces to the same writes, you're kind of asking to be drawn into this again — which is exactly how I found it before reading a single function body.
Written by
MJ — offensive security researcher
I break things in a lab so defenders can build better controls. Everything published here is lab-derived, responsibly disclosed, and documented with the detection path in mind.