Skip to main content
VulnerabilityPublished

CVE-2026-42238: Nginx-UI is Vulnerable to Unauthenticated Remote Code Execution via Backup Restore

Affects github.com/0xJacky/nginx-uiSource GCVE Database

0
Authoritative record:CVE-2026-42238· Critical 9.0

Not known to be exploited · CVSS 9.0 Critical : upgrade to 2.3.8 as a priority.

Data as of 27 Oct 2026, 00:07 UTC

CVSS 4.0 base score
9.0Critical
How severe the flaw is on the 0–10 CVSS scale.
Patch
Available – fixed in 2.3.8
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 4 May 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponised: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 26 Oct 2026.

What to do

Upgrade github.com/0xJacky/nginx-ui to 2.3.8.

Affected software

ProductAffectedFixed in
Gogithub.com/0xJacky/nginx-uiAffected
  • <2.3.8
Fixed in
  • 2.3.8

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

Description

**Product:** nginx-ui **Repository:** `0xJacky/nginx-ui` (branch: `dev`) **Vulnerability Class:** Authentication Bypass → Arbitrary File Write → OS Command Injection **Affected Component:** `POST /api/restore` --- ## 1. Vulnerability Summary nginx-ui exposes a backup restore endpoint (`POST /api/restore`) that is **completely unauthenticated** during the first 10 minutes after process startup on any fresh installation. An unauthenticated remote attacker can upload a crafted backup archive that overwrites the application's configuration file (`app.ini`) and SQLite database. Because the attacker controls the restored `app.ini`, they can inject an arbitrary OS command into the `TestConfigCmd` setting. After the application automatically restarts to apply the restored config, a single follow-up request triggers that command as the user running nginx-ui — typically `root` in Docker deployments. The 10-minute unauthenticated window resets on every process restart, making this exploitable not only on initial deployments but on any restart event (container restart, upgrade, health-check-triggered restart). --- ## 2. Root Cause Analysis ### 2.1 The Restore Route Is Registered Without Authentication `backup.InitRouter` is called on the `root` group, which carries only `IPWhiteList()` middleware — no `AuthRequired()`: [1](#2-0) The route definition: [2](#2-1) ### 2.2 The `authIfInstalled` Guard Has a Time-Bounded Bypass The only authentication guard on the restore route is `authIfInstalled`: [3](#2-2) It calls `AuthRequired()` only when `InstallLockStatus() || IsInstallTimeoutExceeded()` is true. Both conditions are false on a fresh install within the first 10 minutes: [4](#2-3) - `InstallLockStatus()` returns `false` because `JwtSecret` is `""` on a fresh install and `SkipInstallation` defaults to `false`. - `IsInstallTimeoutExceeded()` returns `false` for the first 10 minutes after `startupTime` is set in `init()`. When both are `false`, `authIfInstalled` calls `ctx.Next()` with **zero authentication**. ### 2.3 The `EncryptedForm` Middleware Is Not a Security Barrier The `EncryptedForm()` middleware between `authIfInstalled` and `RestoreBackup` is **optional** — it only activates if the request includes an `encrypted_params` field. If that field is absent, it calls `c.Next()` immediately: [5](#2-4) An attacker sends a plain `multipart/form-data` request without `encrypted_params` and the middleware is a no-op. ### 2.4 The Attacker Controls the AES Key Used to Verify the Backup The restore handler accepts the AES key and IV directly from the attacker via the `security_token` form field: [6](#2-5) The manifest integrity check derives its HMAC signing key **from the attacker-supplied AES key**: [7](#2-6) Since the attacker crafts the backup and supplies the key, they can produce a valid HMAC signature for any manifest content they choose. The integrity check is self-referential and provides no security against a crafted backup. ### 2.5 Restore Overwrites `app.ini` and the SQLite Database Unconditionally When `restore_nginx_ui=true`, `restoreNginxUIConfig` directly copies files from the backup onto disk with no content validation: [8](#2-7) ### 2.6 Restored `TestConfigCmd` Is Executed as a Shell Command After restore, `risefront.Restart()` is called, reloading `app.ini`: [9](#2-8) On the next call to `TestConfig()`, the value of `TestConfigCmd` from the restored `app.ini` is passed verbatim to `/bin/sh -c`: [10](#2-9) [11](#2-10) --- ## 3. Attack Prerequisites | Requirement | Notes | |---|---| | Network access to nginx-ui port | Default: 9000/tcp | | Target is a fresh install | `JwtSecret` is empty in `app.ini` | | Within 10 minutes of last process start | Window resets on every restart | | IP not blocked by `IPWhiteList` | Default config has no IP whitelist | The 10-minute window is not a meaningful mitigation in practice. Docker containers restart frequently due to health checks, upgrades, and orchestrator rescheduling. Any restart resets `startupTime` via `init()`, reopening the window. --- ## 4. Step-by-Step Proof of Concept ### Step 1 — Confirm the installation window is open ```http GET /api/install HTTP/1.1 Host: target:9000 ``` Expected response confirming vulnerability: ```json {"lock": false, "timeout": false} ``` ### Step 2 — Craft the malicious backup The backup format (derived from `internal/backup/backup.go`) is: ``` backup-TIMESTAMP.zip ← outer ZIP (unencrypted) ├── manifest.json ← JSON manifest ├── manifest.sig ← HMAC-SHA256 of manifest.json ├── nginx-ui.zip ← AES-CBC encrypted inner ZIP └── nginx.zip ← AES-CBC encrypted inner ZIP ``` **2a.** Generate a random 32-byte AES key and 16-byte IV. **2b.** Create the malicious `app.ini` to place inside `nginx-ui.zip`: ```ini [app] JwtSecret = attacker_chosen_jwt_secret_32chars [node] Secret = attacker_chosen_node_secret [nginx] TestConfigCmd = cur

References

Community reviews

Loading reviews…