GitHub Security Advisory: GHSA-4pvg-prr3-9cxr
Package: github.com/0xJacky/nginx-ui (GO)
Severity: CRITICAL (CVSS 9.8)
CWE: CWE-94: Improper Control of Generation of Code ('Code Injection')
Vulnerable versions: < 2.3.8
Patched in: 2.3.8

Summary: Nginx-UI is Vulnerable to Unauthenticated Remote Code Execution via Backup Restore

Details:
**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.

#