
Minimal Python PoC for CVE-2026-40179: injects a malicious metric name via unauthenticated Prometheus remote_write to trigger stored XSS in the web UI. No protobuf stubs required.
remote_writeA minimal, dependency-light proof of concept for CVE-2026-40179 (GHSA-vffh-x6r8-xx99): stored cross-site scripting in the Prometheus web UI, delivered through an unauthenticated remote_write endpoint.
Prometheus 3.x renders metric names and label values into the web UI via innerHTML without escaping. Because Prometheus v3 relaxed label validation to allow arbitrary UTF-8 — including <, >, and " — a metric name can carry an HTML payload that executes when someone browses the UI.
| Affected | Prometheus 3.0.0 – 3.5.1 |
| Fixed in | 3.5.2 |
| Impact | Stored XSS, executes in the browser of any user viewing the affected metric |
| Vulnerable surfaces | Graph page tooltips, Metrics Explorer |
The XSS itself needs a way to get a crafted series into the TSDB. Any of these will do:
remote_write endpoint (what this PoC uses),An internet- or LAN-exposed Prometheus with no authentication in front of it gives all of the above for free, which is what makes this practically exploitable rather than theoretical.
Sends a single POST /api/v1/write containing one time series whose __name__ is:
pentest_poc_cve_2026_40179
The protobuf WriteRequest is hand-encoded in ~40 lines, so there is no protobuf, prometheus_pb2, or promtool dependency — just python-snappy for the wire compression and requests for the POST. That makes it easy to drop onto a jump host where you can't build generated protobuf stubs.
python3 -m pip install python-snappy requests
python-snappy needs the Snappy C library:
# Debian/Ubuntu
sudo apt install libsnappy-dev
# macOS
brew install snappy
python3 cve-2026-40179-poc.py <target_ip>
The script assumes HTTPS on port 9090 with certificate verification disabled (typical for an internal instance with a self-signed cert). If your target is plain HTTP or on another port, edit the url line in main().
A successful injection returns HTTP 204:
POST https://192.0.2.10:9090/api/v1/write -> 204
Query the series in the Prometheus UI or API — it should return immediately:
{__name__=~"pentest_poc.*"}
The Table view escapes it correctly and shows the payload as plain text. That is expected and is not the vulnerable surface.
Open the Metrics Explorer (the metric browser next to the query box), search for pentest_poc, and hover the listed entry.
In field testing against 3.2.1, the Metrics Explorer path fired reliably while the Graph tab tooltip did not reproduce — the injected sample (value 1.0) was not visibly rendered on the plotted chart, and hovering the legend did not trigger it either. Both surfaces are named in the advisory; if one doesn't fire on your target version, try the other before concluding it isn't vulnerable.
The default payload uses console.log() so it doesn't interrupt anyone who isn't looking for it. For a visible demo — a screenshot for a report, for example — change METRIC_NAME to use alert():
METRIC_NAME = ('pentest_poc_cve_2026_40179'
'')
The injected series is persistent. Once written, it stays in the TSDB until retention expires it. Removing it requires the admin API, which is disabled by default:
# Only works if Prometheus was started with --web.enable-admin-api
curl -X POST -g 'https://<target>:9090/api/v1/admin/tsdb/delete_series?match[]={__name__=~"pentest_poc.*"}'
curl -X POST 'https://<target>:9090/api/v1/admin/tsdb/clean_tombstones'
If the admin API is off, there is no remote cleanup path. The only alternative is promtool tsdb run locally on the host, which you probably don't have access to.
Practical consequences:
job="pentest_poc") so it is easy to find and obviously a test artifact.To reproduce against a disposable instance rather than anything live:
docker run --rm -p 9090:9090 prom/prometheus:v3.2.1 \
--config.file=/etc/prometheus/prometheus.yml \
--web.enable-remote-write-receiver \
--web.enable-admin-api
Then point the script at 127.0.0.1 (change the URL scheme to http:// in main()). Starting with --web.enable-admin-api means you can actually delete the series afterwards.
--web.config.file with basic_auth_users.--web.enable-remote-write-receiver unless you need it, and never expose it unauthenticated.Note that (1) alone fixes the XSS, but an unauthenticated Prometheus remains a substantial information-disclosure surface on its own: the full metric name list, scrape target inventory, and internal hostnames are all readable without credentials.
This is published for defenders, researchers, and testers working under authorization. The vulnerability is publicly disclosed and patched upstream.
Running this against a system writes persistent data to it. Do not point it at infrastructure you do not own or have explicit written permission to test.
MIT — see LICENSE.