CVE-2025-23047 — Hubble UI wildcard CORS information disclosure¶
Published 2026-06-06
Verified reproduction
A headless-browser cross-origin read confirmed this vulnerability is exploitable against affected Cilium releases. The fresh per-boot secret was recovered from a foreign origin by the exploit, then independently confirmed through a privileged server-side channel.
| Field | Value |
|---|---|
| Project | Cilium |
| Affected component | Hubble UI nginx reverse proxy (install/kubernetes/cilium/templates/hubble-ui/_nginx.tpl) |
| Severity | MEDIUM |
| CVSS | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N (6.5) |
| CWE | CWE-200 |
| Affected versions | v1.14.0–v1.14.18, v1.15.0–v1.15.12, v1.16.0–v1.16.5 |
| Fixed version | v1.14.19, v1.15.13, v1.16.6 |
| Advisory | GHSA-h78m-j95m-5356 |
1. Vulnerability overview¶
Cilium is a Kubernetes CNI and networking project written in Go. Its Hubble UI component (a browser-based visualization of cluster network traffic) ships an nginx reverse proxy whose configuration is generated from a Helm chart template. In affected versions, that template unconditionally injected a permissive Access-Control-Allow-Origin: * header into every nginx response, including responses that proxied Hubble UI's internal API. Any web page loaded from a foreign origin could therefore issue cross-origin requests to the Hubble UI API and read the JSON responses in full. An attacker who lured an authenticated Hubble UI user to visit an attacker-controlled page would receive cluster topology information (node names, workload IP addresses, network policy metadata) exfiltrated directly by the victim's browser.
Root cause¶
The vulnerability is in the Helm template file install/kubernetes/cilium/templates/hubble-ui/_nginx.tpl. When rendered, it produced an nginx server {} block containing the following CORS directives at the top level, before any location block:
# CORS
add_header Access-Control-Allow-Methods "GET, POST, PUT, HEAD, DELETE, OPTIONS";
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Max-Age 1728000;
add_header Access-Control-Expose-Headers content-length,grpc-status,grpc-message;
add_header Access-Control-Allow-Headers range,keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout;
if ($request_method = OPTIONS) {
return 204;
}
# /CORS
The /api location block separately contained proxy_hide_header Access-Control-Allow-Origin;, intended to suppress any Access-Control-Allow-Origin header coming from the upstream backend. This directive only suppresses upstream response headers, though; it has no effect on headers that nginx itself adds with add_header. Because the server-level add_header Access-Control-Allow-Origin * had already written the wildcard value into the response, the browser received it regardless of what the /api block tried to hide. Browsers seeing Access-Control-Allow-Origin: * on an API response allow any origin to read the body.
The fix (patch commit a3489f190ba6e87b5336ee685fb6c80b1270d06d, merged 2024-11-04, authored by Dmitry Kharitonov) is a removal-only change: the entire CORS block and the proxy_hide_header directive were deleted. Without an Access-Control-Allow-Origin header, browsers apply the same-origin policy and block cross-origin reads of API responses.
2. Vulnerable environment¶
The reproduction environment uses two Docker containers connected on a private bridge network (env_cve-net). No Kubernetes cluster is needed; the defect is in the rendered nginx configuration, not in compiled Cilium binaries.
Download the environment files: env.zip
| Container | Role | Origin |
|---|---|---|
cve-2025-23047-hubble-ui |
Vulnerable Hubble UI nginx + stub /api backend (target, origin A) |
http://127.0.0.1:8081 |
cve-2025-23047-attacker |
Static web server hosting the attacker page (origin B) | http://127.0.0.1:8088 |
The hubble-ui container runs the nginx config rendered from the affected v1.16.5 Helm tag (env/config/nginx.conf.vulnerable) and a small Python stub backend (env/config/backend.py) that listens on loopback port 8090 and mirrors the real Hubble UI topology. The attacker container is a plain nginx serving a static HTML page from a different port, establishing the distinct origin that exercises the browser's same-origin policy.
Fresh secret mechanism. On every container start, the entrypoint script (env/config/entrypoint.sh) generates a random UUID and writes it to /run/hubble-secret inside the container. The stub backend reads that file and includes the value in the cluster_token field of every /api/* JSON response. This value is never baked at build time, so an exploit cannot know it in advance. It can only appear in the exploit's output if the cross-origin browser read genuinely succeeded.
Standing up the environment:
Key files: env/Dockerfile, env/docker-compose.yml, env/config/nginx.conf.vulnerable, env/config/entrypoint.sh, env/config/backend.py.
Smoke test — confirming you have the vulnerable environment:
# Both containers running:
docker compose -f env/docker-compose.yml ps
# Target API reachable; corroborate the wildcard header (server-side only):
curl -s -D - http://127.0.0.1:8081/api/cluster | grep -i access-control-allow-origin
# Read the true current fresh secret via the privileged channel (never via origin B):
docker exec cve-2025-23047-hubble-ui cat /run/hubble-secret
# Attacker origin reachable:
curl -s http://127.0.0.1:8088/ | head -1
The curl on the API endpoint should show Access-Control-Allow-Origin: *, the server-side signature of the misconfiguration. Observing this header on its own is corroboration; proving the bug requires a genuine cross-origin browser read as described in §3.
3. How to exploit¶
The exploit drives a real headless Chrome browser (via puppeteer-core) because the vulnerability is enforced by the browser's same-origin policy. A plain HTTP client like curl ignores CORS headers and cannot demonstrate whether a cross-origin browser read succeeds or fails.
Download the exploit files: exploit.zip
Exploit files: exploit/run.sh, exploit/fetch.js.
Steps¶
Step 1. Confirm the environment is up and the target is healthy:
Expected output: ok.
Step 2. Run the exploit. This is a single shell invocation that provisions puppeteer-core locally, launches headless Chrome, navigates it to origin B (http://127.0.0.1:8088), and from that page issues a cross-origin fetch to the Hubble UI API on origin A:
bash exploit/run.sh "http://127.0.0.1:8088" "http://127.0.0.1:8081/api/cluster" "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
The three arguments are the attacker origin the browser is navigated to, the Hubble UI API endpoint to read, and the path to the system Chrome executable (run.sh drives it without downloading a separate Chromium).
Step 3. Capture the cluster_token printed to stdout and compare it with the privileged-channel value:
What proves it worked¶
The verifier independently reads the true current secret via the privileged channel (docker exec … cat /run/hubble-secret), a server-side path that the exploit itself never touches. This independent observation confirms the result is from a genuine cross-origin browser read rather than any side channel.
Observed exploit output from the verified reproduction run:
Stderr (exploit progress):
[*] document origin: http://127.0.0.1:8088 (nav status 200 )
[*] cross-origin target: http://127.0.0.1:8081/api/cluster
[*] credentials:include attempt -> blocked: TypeError: Failed to fetch
[+] cross-origin read SUCCEEDED (status 200) from foreign origin http://127.0.0.1:8088 -- body follows:
Stdout (the JSON body the browser read cross-origin):
{"cluster": "hubble-lab", "namespaces": ["kube-system", "default"], "cluster_token": "0538235c-8021-45a0-a42e-fb9335a1f2d6"}
Privileged-channel truth (separate read):
The cluster_token in the exploit's stdout (0538235c-8021-45a0-a42e-fb9335a1f2d6) exactly matches the value read through the privileged channel. The secret was freshly generated at container start (a prior boot produced ab076580-f74e-4779-a924-e085a719fafc, a different value), so it could not have been guessed or planted.
The credentials:'include' attempt on stderr was intentionally blocked: the CORS specification disallows credentialed reads when Access-Control-Allow-Origin is a wildcard rather than a specific origin. The exploit's discriminating step is the plain cross-origin GET, which the wildcard ACAO allows. A patched build (no ACAO header at all) would block this at the browser's same-origin policy enforcement layer, returning a TypeError with no body.
Environment teardown¶
4. Security advice¶
Remediation¶
Upgrade to a fixed release: v1.14.19, v1.15.13, or v1.16.6. The fix removes the entire CORS add_header block from the Hubble UI nginx template. Without an Access-Control-Allow-Origin header, browsers apply the same-origin policy and refuse cross-origin reads of the API.
If an immediate upgrade is not possible, a short-term workaround is to manually render the Hubble UI nginx configuration with the CORS block deleted, or to deploy Hubble UI behind an ingress or network policy that prevents exposure to user browsers from untrusted origins. Hubble UI should never be exposed to the public internet.
Context¶
The CVSS UI:R condition reflects that exploitation requires luring an authenticated Hubble UI user to visit an attacker-controlled page. In practice the attack surface is bounded by who has access to Hubble UI in a given cluster. The exposed data (node names, workload IPs, namespace structure) is sensitive cluster topology information that an attacker could use to plan further lateral movement.
The attempt to suppress the header with proxy_hide_header Access-Control-Allow-Origin inside the /api location block illustrates a common nginx misunderstanding: proxy_hide_header operates on upstream response headers, not on headers that nginx itself sets via add_header. Server-level add_header directives propagate into nested location blocks, and proxy_hide_header cannot override them.
References¶
- GHSA-h78m-j95m-5356 — affected and fixed versions, CVSS vector, CWE classification, advisory description
- NVD CVE-2025-23047 — CWE-200 confirmation (note: NVD's fixed-version list conflicts with GHSA; GHSA is authoritative)
- Patch commit a3489f19 — full diff showing the CORS block removal in
install/kubernetes/cilium/templates/hubble-ui/_nginx.tpl - BUseclab/cve-genie exploit.py — machine-generated PoC demonstrating the header-presence check