Nginx UI: Backup restore follows crafted symlinks into the live Nginx configuration path before restore flags are applied
CVE-2026-107810 · GHSA-p8v3-89rh-jxc7 · GO-2026-6707 · published 9 Oct 2026
How serious is it
- Severity: high, CVSS score 8.1 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-107810?
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.20250517140552-daee3ac7ade1, < 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 user who can create and restore a backup can craft a valid backup archive that causes the restore staging process to write attacker-controlled files into the live Nginx configuration path even when both `restore_nginx` and `restore_nginx_ui` are set to `false`.
### Details
The restore flow always extracts the outer archive, verifies the manifest, decrypts `nginx-ui.zip` and `nginx.zip`, and extracts both inner archives before it decides whether `RestoreNginx` or `RestoreNginxUI` should be applied. The zip extractor explicitly allows absolute symlinks when the link target is under `nginx.GetConfPath()` or `nginx.GetModulesPath()`. Later regular-file entries are then created with `os.OpenFile()` on the symlinked path, which follows the symlink and writes into the live path.
Relevant code paths:
- [internal/backup/restore.go](/home/kali/Desktop/bounty/nginx-ui/internal/backup/restore.go:39)
- [internal/backup/restore.go](/home/kali/Desktop/bounty/nginx-ui/internal/backup/restore.go:79)
- [internal/backup/restore.go](/home/kali/Desktop/bounty/nginx-ui/internal/backup/restore.go:204)
- [internal/backup/restore.go](/home/kali/Desktop/bounty/nginx-ui/internal/backup/restore.go:286)
- [api/backup/restore.go](/home/kali/Desktop/bounty/nginx-ui/api/backup/restore.go:31)
- [api/backup/backup.go](/home/kali/Desktop/bounty/nginx-ui/api/backup/backup.go:15)
This means the restore trust boundary is broken during extraction. A restore request that explicitly opted out of restoring either Nginx or Nginx UI can still modify the live Nginx configuration tree during staging.
### PoC
I verified this locally in an isolated environment with a temporary package-level harness that exercised the real `Backup()` and `Restore()` implementations.
What the executed test did:
1. Created a temporary `app.ini`, database file, and a temporary live Nginx config directory.
2. Called the real `Backup()` implementation to obtain a valid backup archive plus AES key/IV.
3. Extracted the outer backup, decrypted `nginx.zip`, replaced it with a crafted zip containing:
- a symlink entry `link -> <live nginx conf dir>`
- a later regular file entry `link/poc.conf`
4. Recomputed `manifest.json` size/hash values for the modified encrypted `nginx.zip` and re-signed `manifest.sig` with the expected signing key derived from the AES key.
5. Repacked the outer archive and called the real `Restore()` implementation with:
- `RestoreNginx: false`
- `RestoreNginxUI: false`
6. Verified that `<live nginx conf dir>/poc.conf` was created anyway.
Observed result from the actual local verification:
- The crafted restore completed successfully with both restore flags set to `false`.
- The asserted sink was the existence and content of the live-path file written during restore staging.
### Impact
Any deployment that allows an authenticated user to create and restore backups is affected. A crafted restore archive can modify the live Nginx configuration path before either restore toggle is honored. This can lead to persistent configuration injection, denial of service on a later reload, or other follow-on impact depending on what files the deployment later consumes from the modified path.
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-p8v3-89rh-jxc7
- github.com/0xJacky/nginx-ui/commit/a467ed652591fc0cd1b466a1ec751b493faef9f7
- github.com/0xJacky/nginx-ui/releases/tag/v2.5.0
- github.com/advisories/GHSA-p8v3-89rh-jxc7
- github.com/0xJacky/nginx-ui
Merged from GitHub Advisory Database, OSV. Last updated 24 Oct 2026. All vulnerabilities · RSS · JSON