Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions packages/pickled-core/src/pickled_core/mcp/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,40 @@

from __future__ import annotations

import ipaddress
from typing import Any, Literal


def _is_unspecified_address(host: str) -> bool:
"""Return True iff ``host`` is an IPv4/IPv6 wildcard ("unspecified") address.

The previous implementation only rejected the literal string ``0.0.0.0``,
which let callers bind to ``::`` / ``[::]`` (the IPv6 wildcard) and bypass
the "no public binding without --allow-public" guard. That is a real
accidental-exposure footgun on any host with a routable IPv6 address: the
OS happily listens on every IPv6 interface, including public ones, and
typically accepts IPv4 traffic too via dual-stack mapping.

We accept the bracketed form ``[::]`` (uvicorn / FastMCP take it as well),
fully-expanded forms such as ``0:0:0:0:0:0:0:0``, and the IPv4-mapped
wildcard ``::ffff:0.0.0.0``. Whitespace is stripped so the heuristic is
not defeated by trivial copy-paste artifacts. Hostnames are intentionally
left untouched — we deliberately do not resolve DNS in this guard.
"""
stripped = host.strip()
if stripped.startswith("[") and stripped.endswith("]"):
stripped = stripped[1:-1]
try:
addr = ipaddress.ip_address(stripped)
except ValueError:
return False
if addr.is_unspecified:
return True
if isinstance(addr, ipaddress.IPv6Address) and addr.ipv4_mapped is not None:
return addr.ipv4_mapped.is_unspecified
return False


def resolve_transport(
name: Literal["stdio", "http"],
host: str | None,
Expand All @@ -16,9 +47,10 @@ def resolve_transport(
return {"transport": "stdio"}
if name == "http":
actual_host = host or "127.0.0.1"
if actual_host == "0.0.0.0" and not allow_public:
if _is_unspecified_address(actual_host) and not allow_public:
raise RuntimeError(
"refusing to bind 0.0.0.0 without --allow-public (irreversible network exposure)"
f"refusing to bind wildcard address {actual_host!r} without "
"--allow-public (irreversible network exposure)"
)
return {
"transport": "streamable-http",
Expand Down
42 changes: 42 additions & 0 deletions packages/pickled-core/tests/test_mcp_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,45 @@ def test_allow_public_bind() -> None:
kw = resolve_transport("http", "0.0.0.0", 9000, True)
assert kw["host"] == "0.0.0.0"
assert kw["port"] == 9000


@pytest.mark.parametrize(
"host",
[
"::",
"[::]",
"0:0:0:0:0:0:0:0",
"::ffff:0.0.0.0",
" 0.0.0.0 ",
" :: ",
],
)
def test_refuse_ipv6_and_padded_wildcards_without_flag(host: str) -> None:
"""Any IPv4/IPv6 wildcard ('unspecified') host must require --allow-public.

The previous guard only blocked the literal ``0.0.0.0``; binding to ``::``
silently exposed the MCP server on every IPv6 interface, defeating the
intent of the safety check.
"""
with pytest.raises(RuntimeError, match="wildcard"):
resolve_transport("http", host, None, False)


@pytest.mark.parametrize(
"host",
["::", "[::]", "::ffff:0.0.0.0"],
)
def test_allow_ipv6_wildcards_with_flag(host: str) -> None:
kw = resolve_transport("http", host, 7802, True)
assert kw["host"] == host
assert kw["port"] == 7802


@pytest.mark.parametrize(
"host",
["127.0.0.1", "::1", "[::1]", "localhost", "192.168.1.10", "::ffff:127.0.0.1"],
)
def test_allow_loopback_and_specific_hosts_without_flag(host: str) -> None:
"""Loopback addresses and specific hostnames must not be misidentified as wildcards."""
kw = resolve_transport("http", host, None, False)
assert kw["host"] == host
Loading