{"id":"PYSEC-2026-2625","summary":"mcp-memory-service's Wildcard CORS with Credentials Enables Cross-Origin Memory Theft","details":"### Summary\nWhen the HTTP server is enabled (`MCP_HTTP_ENABLED=true`), the application configures FastAPI's CORSMiddleware with `allow_origins=['*']`, `allow_credentials=True`, `allow_methods=[\"*\"]`, and `allow_headers=[\"*\"]`. The wildcard `Access-Control-Allow-Origin: *` header permits any website to read API responses cross-origin. When combined with anonymous access (`MCP_ALLOW_ANONYMOUS_ACCESS=true`) - the simplest way to get the HTTP dashboard working without OAuth - no credentials are needed, so any malicious website can silently read, modify, and delete all stored memories.\n\n\n### Details\n### Vulnerable Code\n\n**`config.py:546` - Wildcard CORS origin default**\n\n```python\nCORS_ORIGINS = os.getenv('MCP_CORS_ORIGINS', '*').split(',')\n```\n\nThis produces `['*']` by default, allowing any origin.\n\n**`app.py:274-280` - CORSMiddleware configuration**\n\n```python\n# CORS middleware\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=CORS_ORIGINS,        # ['*'] by default\n    allow_credentials=True,             # Unnecessary for anonymous access; bad practice\n    allow_methods=[\"*\"],\n    allow_headers=[\"*\"],\n)\n```\n\n### How the Attack Works\n\nThe wildcard CORS default means every API response includes `Access-Control-Allow-Origin: *`. This tells browsers to allow **any website** to read the response. When combined with anonymous access (no authentication required), the attack is straightforward:\n\n```javascript\n// Running on https://evil.com - reads victim's memories\n// No credentials needed - anonymous access means the API is open\nconst response = await fetch('http://192.168.1.100:8000/api/memories');\nconst memories = await response.json();\n// memories contains every stored memory - passwords, API keys, personal notes\n```\n\nThe browser sends the request, the server responds with `ACAO: *`, and the browser allows the JavaScript to read the response body. No cookies, no auth headers, no credentials of any kind.\n\n**Clarification on `allow_credentials=True`:** The advisory originally stated that Starlette reflects the `Origin` header when `allow_credentials=True` with wildcard origins. Testing with Starlette 0.52.1 shows that **actual responses return `ACAO: *`** (not the reflected origin); only preflight `OPTIONS` responses reflect the origin. Per the Fetch specification, browsers block `ACAO: *` when `credentials: 'include'` is used. However, this is irrelevant to the attack because **anonymous access means no credentials are needed** - a plain `fetch()` without `credentials: 'include'` works, and `ACAO: *` allows it.\n\n### Two Attack Vectors\n\nThis misconfiguration enables two distinct attack paths:\n\n**1. Cross-origin browser attack (CORS - this advisory)**\n- Attacker lures victim to a malicious webpage\n- JavaScript on the page reads/writes the memory service API\n- Works from anywhere on the internet if the victim visits the page\n- The `ACAO: *` header is what allows the browser to expose the response to the attacker's JavaScript\n\n**2. Direct network access (compounding factor)**\n- Attacker on the same network directly calls the API (`curl http://\u003ctarget\u003e:8000/api/memories`)\n- No CORS involved - CORS is a browser-only restriction\n- Enabled by `0.0.0.0` binding + anonymous access, independent of CORS configuration\n\nThe CORS misconfiguration specifically enables attack vector #1, extending the reach from local network to anyone who can get the victim to click a link.\n\n### Compounding Factors\n\n- **`HTTP_HOST = '0.0.0.0'`** - Binds to all interfaces, exposing the service to the entire network (enables attack vector #2)\n- **`HTTPS_ENABLED = 'false'`** - No TLS by default, allowing passive interception\n- **`MCP_ALLOW_ANONYMOUS_ACCESS`** - When enabled, no authentication is required at all. This is the key enabler: without it, the CORS wildcard alone would not allow data access (the attacker would need to forward valid credentials, which `ACAO: *` blocks)\n- **`allow_credentials=True`** - Bad practice: if a future Starlette version changes to reflect origins (as some CORS implementations do), this would escalate the vulnerability by allowing credential-forwarding attacks against OAuth/API-key users\n- **API key via query parameter** - `api_key` query param is cached in browser history and server logs\n\n### Attack Scenario\n\n1. Victim runs `mcp-memory-service` with HTTP enabled and anonymous access\n2. Victim visits `https://evil.com` which includes JavaScript\n3. JavaScript sends `fetch('http://\u003cvictim-ip\u003e:8000/api/memories')` (no credentials needed)\n4. Server responds with `Access-Control-Allow-Origin: *`\n5. Browser allows JavaScript to read the response - attacker receives all memories\n6. Attacker's script also calls DELETE/PUT endpoints to modify or destroy memories\n7. Victim sees a normal web page; no indication of the attack\n\n### Root Cause\n\nThe default value of `MCP_CORS_ORIGINS` is `*`, which allows any website to read API responses. This is a permissive default that should be restricted to the expected dashboard origin (typically `localhost`). The `allow_credentials=True` is an additional misconfiguration that doesn't currently enable the attack.\n\n\n### PoC\n```python\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom starlette.testclient import TestClient\n\napp = FastAPI()\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\"*\"],\n    allow_credentials=True,\n    allow_methods=[\"*\"],\n    allow_headers=[\"*\"],\n)\n\n@app.get(\"/api/memories\")\ndef memories():\n    return [{\"content\": \"secret memory data\"}]\n\nclient = TestClient(app)\n\n# Non-credentialed request (how the real attack works with anonymous access)\nresponse = client.get(\"/api/memories\", headers={\"Origin\": \"https://evil.com\"})\nprint(response.headers[\"access-control-allow-origin\"])  # *\nprint(response.json())  # [{\"content\": \"secret memory data\"}]\n# Any website can read this response because ACAO is *\n```\n\n\n### Impact\n- **Complete cross-origin memory access**: Any website can read all stored memories when the victim has the HTTP server running with anonymous access\n- **Memory tampering**: Write/delete endpoints are also accessible cross-origin, allowing memory destruction\n- **Remote attack surface**: Unlike direct network access (which requires LAN proximity), the CORS vector works from anywhere on the internet - the victim just needs to visit a link\n- **Silent exfiltration**: The attack is invisible to the victim; no browser warnings, no popups, no indicators\n\n## Remediation\n\nReplace the wildcard default with an explicit localhost origin:\n\n```python\n# In config.py  (safe default)\nCORS_ORIGINS = os.getenv('MCP_CORS_ORIGINS', 'http://localhost:8000,http://127.0.0.1:8000').split(',')\n\n# In app.py - warn on wildcard\nif '*' in CORS_ORIGINS:\n    logger.warning(\"Wildcard CORS origin detected. This allows any website to access the API. \"\n                    \"Set MCP_CORS_ORIGINS to restrict access.\")\n\n# Also: set allow_credentials=False unless specific origins are configured\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=CORS_ORIGINS,\n    allow_credentials='*' not in CORS_ORIGINS,  # Only with explicit origins\n    allow_methods=[\"*\"],\n    allow_headers=[\"*\"],\n)\n```\n\n## Affected Deployments\nThe vulnerability exists in the Python source code and is not mitigated by any deployment-specific configuration. Docker HTTP mode is the highest-risk deployment because it explicitly binds to `0.0.0.0`, maps the port, and does not override the wildcard CORS default.","aliases":["CVE-2026-33010","GHSA-g9rg-8vq5-mpwm"],"modified":"2026-07-13T16:32:13.301820998Z","published":"2026-07-13T14:36:41.458247Z","references":[{"type":"WEB","url":"https://github.com/doobidoo/mcp-memory-service/security/advisories/GHSA-g9rg-8vq5-mpwm"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33010"},{"type":"PACKAGE","url":"https://github.com/doobidoo/mcp-memory-service"},{"type":"PACKAGE","url":"https://pypi.org/project/mcp-memory-service"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-g9rg-8vq5-mpwm"}],"affected":[{"package":{"name":"mcp-memory-service","ecosystem":"PyPI","purl":"pkg:pypi/mcp-memory-service"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.25.1"}]}],"versions":["10.0.0","10.0.1","10.0.2","10.0.3","10.1.0","10.1.1","10.1.2","10.10.0","10.10.1","10.10.2","10.10.3","10.10.4","10.10.5","10.10.6","10.11.0","10.11.1","10.11.2","10.12.0","10.12.1","10.13.0","10.13.1","10.13.2","10.14.0","10.15.0","10.15.1","10.16.0","10.16.1","10.17.0","10.17.10","10.17.12","10.17.13","10.17.14","10.17.15","10.17.16","10.17.2","10.17.3","10.17.4","10.17.5","10.17.6","10.17.7","10.17.8","10.17.9","10.18.0","10.18.1","10.18.2","10.18.3","10.19.0","10.2.0","10.2.1","10.20.0","10.20.1","10.20.2","10.20.4","10.20.5","10.20.6","10.21.0","10.21.1","10.22.0","10.23.0","10.24.0","10.25.0","10.3.0","10.4.0","10.4.1","10.4.2","10.4.3","10.4.4","10.4.5","10.4.6","10.5.0","10.5.1","10.6.0","10.6.1","10.7.0","10.7.1","10.7.2","10.8.0","10.9.0","8.24.0","8.25.1","8.25.2","8.26.0","8.27.0","8.27.1","8.27.2","8.28.0","8.28.1","8.29.0","8.30.0","8.31.0","8.32.0","8.33.0","8.34.0","8.35.0","8.36.0","8.36.1","8.37.0","8.38.0","8.38.1","8.39.0","8.39.1","8.40.0","8.41.0","8.41.1","8.41.2","8.42.0","8.42.1","8.43.0","8.44.0","8.45.0","8.45.1","8.45.2","8.45.3","8.46.0","8.46.1","8.46.2","8.46.3","8.47.0","8.47.1","8.48.0","8.48.1","8.48.2","8.48.3","8.48.4","8.49.0","8.50.0","8.50.1","8.51.0","8.52.0","8.52.1","8.52.2","8.53.0","8.54.0","8.54.1","8.54.2","8.54.3","8.54.4","8.55.0","8.57.1","8.58.0","8.59.0","8.60.0","8.61.0","8.61.1","8.61.2","8.62.0","8.62.1","8.62.10","8.62.11","8.62.12","8.62.13","8.62.2","8.62.3","8.62.4","8.62.5","8.62.6","8.62.7","8.62.8","8.62.9","8.63.0","8.63.1","8.64.0","8.65.0","8.66.0","8.67.0","8.68.0","8.68.1","8.68.2","8.69.0","8.70.0","8.71.0","8.72.0","8.73.0","8.74.0","8.75.0","8.75.1","8.76.0","9.0.0","9.0.1","9.0.2","9.0.3","9.0.4","9.0.5","9.0.6","9.2.0","9.2.1","9.3.1"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/mcp-memory-service/PYSEC-2026-2625.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N"}]}