## https://sploitus.com/exploit?id=CEDAF4CF-2A76-5AD8-9330-AC53CF640CAA
# CVE-2026-40179 β Prometheus stored XSS via `remote_write`
A minimal, dependency-light proof of concept for **CVE-2026-40179** ([GHSA-vffh-x6r8-xx99](https://github.com/advisories/GHSA-vffh-x6r8-xx99)): stored cross-site scripting in the Prometheus web UI, delivered through an unauthenticated `remote_write` endpoint.
## The vulnerability
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:
- an open `remote_write` endpoint (what this PoC uses),
- a compromised or attacker-controlled scrape target,
- an OTLP receiver.
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.
## What this script does
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.
## Requirements
```sh
python3 -m pip install python-snappy requests
```
`python-snappy` needs the Snappy C library:
```sh
# Debian/Ubuntu
sudo apt install libsnappy-dev
# macOS
brew install snappy
```
## Usage
```sh
python3 cve-2026-40179-poc.py
```
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
```
### Confirming the write landed
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.
### Triggering the payload
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()`:
```python
METRIC_NAME = ('pentest_poc_cve_2026_40179'
'')
```
## Cleanup β read before you run this
**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**:
```sh
# Only works if Prometheus was started with --web.enable-admin-api
curl -X POST -g 'https://:9090/api/v1/admin/tsdb/delete_series?match[]={__name__=~"pentest_poc.*"}'
curl -X POST 'https://: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:
- Confirm cleanup is possible *before* injecting into anything you care about, or accept that the artifact persists.
- The metric name is deliberately long, unmistakable, and self-labelling (`job="pentest_poc"`) so it is easy to find and obviously a test artifact.
- On an authorized engagement, tell the client about any residual series directly, so it isn't later mistaken for a real compromise.
## Testing safely
To reproduce against a disposable instance rather than anything live:
```sh
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.
## Remediation
1. **Upgrade to Prometheus 3.5.2 or later.** This is the fix.
2. Put authentication and TLS in front of the Prometheus UI and API β a reverse proxy with auth, or `--web.config.file` with `basic_auth_users`.
3. Don't enable `--web.enable-remote-write-receiver` unless you need it, and never expose it unauthenticated.
4. Restrict network access to port 9090 to the hosts that legitimately need it.
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.
## Authorized use only
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.
## License
MIT β see [LICENSE](LICENSE).