PYSEC-2026-2625

Vulnerability from pysec - Published: 2026-07-13 14:36 - Updated: 2026-07-13 16:04
VLAI
Details

Summary

When 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.

Details

Vulnerable Code

config.py:546 - Wildcard CORS origin default

CORS_ORIGINS = os.getenv('MCP_CORS_ORIGINS', '*').split(',')

This produces ['*'] by default, allowing any origin.

app.py:274-280 - CORSMiddleware configuration

# CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=CORS_ORIGINS,        # ['*'] by default
    allow_credentials=True,             # Unnecessary for anonymous access; bad practice
    allow_methods=["*"],
    allow_headers=["*"],
)

How the Attack Works

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

// Running on https://evil.com - reads victim's memories
// No credentials needed - anonymous access means the API is open
const response = await fetch('http://192.168.1.100:8000/api/memories');
const memories = await response.json();
// memories contains every stored memory - passwords, API keys, personal notes

The 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.

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.

Two Attack Vectors

This misconfiguration enables two distinct attack paths:

1. Cross-origin browser attack (CORS - this advisory) - Attacker lures victim to a malicious webpage - JavaScript on the page reads/writes the memory service API - Works from anywhere on the internet if the victim visits the page - The ACAO: * header is what allows the browser to expose the response to the attacker's JavaScript

2. Direct network access (compounding factor) - Attacker on the same network directly calls the API (curl http://<target>:8000/api/memories) - No CORS involved - CORS is a browser-only restriction - Enabled by 0.0.0.0 binding + anonymous access, independent of CORS configuration

The CORS misconfiguration specifically enables attack vector #1, extending the reach from local network to anyone who can get the victim to click a link.

Compounding Factors

  • HTTP_HOST = '0.0.0.0' - Binds to all interfaces, exposing the service to the entire network (enables attack vector #2)
  • HTTPS_ENABLED = 'false' - No TLS by default, allowing passive interception
  • 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)
  • 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
  • API key via query parameter - api_key query param is cached in browser history and server logs

Attack Scenario

  1. Victim runs mcp-memory-service with HTTP enabled and anonymous access
  2. Victim visits https://evil.com which includes JavaScript
  3. JavaScript sends fetch('http://<victim-ip>:8000/api/memories') (no credentials needed)
  4. Server responds with Access-Control-Allow-Origin: *
  5. Browser allows JavaScript to read the response - attacker receives all memories
  6. Attacker's script also calls DELETE/PUT endpoints to modify or destroy memories
  7. Victim sees a normal web page; no indication of the attack

Root Cause

The 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.

PoC

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.testclient import TestClient

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/api/memories")
def memories():
    return [{"content": "secret memory data"}]

client = TestClient(app)

# Non-credentialed request (how the real attack works with anonymous access)
response = client.get("/api/memories", headers={"Origin": "https://evil.com"})
print(response.headers["access-control-allow-origin"])  # *
print(response.json())  # [{"content": "secret memory data"}]
# Any website can read this response because ACAO is *

Impact

  • Complete cross-origin memory access: Any website can read all stored memories when the victim has the HTTP server running with anonymous access
  • Memory tampering: Write/delete endpoints are also accessible cross-origin, allowing memory destruction
  • 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
  • Silent exfiltration: The attack is invisible to the victim; no browser warnings, no popups, no indicators

Remediation

Replace the wildcard default with an explicit localhost origin:

# In config.py  (safe default)
CORS_ORIGINS = os.getenv('MCP_CORS_ORIGINS', 'http://localhost:8000,http://127.0.0.1:8000').split(',')

# In app.py - warn on wildcard
if '*' in CORS_ORIGINS:
    logger.warning("Wildcard CORS origin detected. This allows any website to access the API. "
                    "Set MCP_CORS_ORIGINS to restrict access.")

# Also: set allow_credentials=False unless specific origins are configured
app.add_middleware(
    CORSMiddleware,
    allow_origins=CORS_ORIGINS,
    allow_credentials='*' not in CORS_ORIGINS,  # Only with explicit origins
    allow_methods=["*"],
    allow_headers=["*"],
)

Affected Deployments

The 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.

Impacted products
Name purl
mcp-memory-service pkg:pypi/mcp-memory-service

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "mcp-memory-service",
        "purl": "pkg:pypi/mcp-memory-service"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "10.25.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "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"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-33010",
    "GHSA-g9rg-8vq5-mpwm"
  ],
  "details": "### Summary\nWhen the HTTP server is enabled (`MCP_HTTP_ENABLED=true`), the application configures FastAPI\u0027s CORSMiddleware with `allow_origins=[\u0027*\u0027]`, `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(\u0027MCP_CORS_ORIGINS\u0027, \u0027*\u0027).split(\u0027,\u0027)\n```\n\nThis produces `[\u0027*\u0027]` 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,        # [\u0027*\u0027] 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\u0027s memories\n// No credentials needed - anonymous access means the API is open\nconst response = await fetch(\u0027http://192.168.1.100:8000/api/memories\u0027);\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: \u0027include\u0027` is used. However, this is irrelevant to the attack because **anonymous access means no credentials are needed** - a plain `fetch()` without `credentials: \u0027include\u0027` 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\u0027s 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 = \u00270.0.0.0\u0027`** - Binds to all interfaces, exposing the service to the entire network (enables attack vector #2)\n- **`HTTPS_ENABLED = \u0027false\u0027`** - 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(\u0027http://\u003cvictim-ip\u003e:8000/api/memories\u0027)` (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\u0027s 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\u0027t 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(\u0027MCP_CORS_ORIGINS\u0027, \u0027http://localhost:8000,http://127.0.0.1:8000\u0027).split(\u0027,\u0027)\n\n# In app.py - warn on wildcard\nif \u0027*\u0027 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=\u0027*\u0027 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.",
  "id": "PYSEC-2026-2625",
  "modified": "2026-07-13T16:04:48.960229Z",
  "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"
    }
  ],
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "mcp-memory-service\u0027s Wildcard CORS with Credentials Enables Cross-Origin Memory Theft"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…