Skip to content

fix(pickled-core): close IPv6 wildcard bypass of MCP --allow-public guard - #27

Merged
bartrosa merged 1 commit into
mainfrom
cursor/critical-correctness-bugs-9b24
May 30, 2026
Merged

bartrosa merged 1 commit into
mainfrom
cursor/critical-correctness-bugs-9b24

Conversation

@cursor

@cursor cursor Bot commented May 27, 2026

Copy link
Copy Markdown

Bug and impact

pickled_core.mcp.transport.resolve_transport is the single chokepoint every pickled-* MCP CLI (pickled-bdd, pickled-rules, pickled-data, pickled-iac, pickled-schema, pickled-diff) goes through to translate --transport http --host ... --port ... [--allow-public] into FastMCP / uvicorn kwargs. Its job is to refuse wildcard binds unless the operator explicitly opts in via --allow-public, because the docstring calls this out as "irreversible network exposure".

The check was an exact-string compare against "0.0.0.0" only:

        actual_host = host or "127.0.0.1"
        if actual_host == "0.0.0.0" and not allow_public:
            raise RuntimeError(
                "refusing to bind 0.0.0.0 without --allow-public (irreversible network exposure)"
            )

That misses the IPv6 wildcard. Running, for example,

pickled-bdd mcp serve --transport http --host ::
pickled-rules mcp serve --transport http --host '[::]'
pickled-data mcp serve --transport http --host 0:0:0:0:0:0:0:0
pickled-iac mcp serve --transport http --host ::ffff:0.0.0.0

silently brings the server up on every IPv6 interface (and, via the kernel's dual-stack acceptance, effectively on every IPv4 interface too on most distros). The user never sees the warning and reasonably believes they're protected. That is exactly the "accidental exposure" the guard was added to prevent. Given the MCP tools include things like validate_terraform_dir, apply_sql_to_sandbox, and (in pickled-diff) verify_against_oracle — which executes arbitrary subprocess commands — making the server publicly reachable is a high-impact security regression. The threat model the project itself adopted (see the module docstring of packages/pickled-rules/src/pickled_rules/mcp_tools.py) is "any pickled-* MCP client should be assumed hostile."

Root cause

A wildcard ("unspecified") address has multiple textual forms, but only the IPv4 literal was being checked. The IPv6 wildcard (::), its bracketed form ([::]), the fully-expanded 0:0:0:0:0:0:0:0, the IPv4-mapped wildcard ::ffff:0.0.0.0, and trivial whitespace padding all behave identically at the socket layer but bypassed the string match.

Fix

Replace the string compare with a structural "is this an unspecified address?" check that uses stdlib ipaddress:

  • Strip surrounding whitespace and IPv6 URI brackets ([::] → ::).
  • Parse with ipaddress.ip_address. Non-IP literals (hostnames, localhost) fall through unchecked — DNS resolution would be too dynamic for this guard.
  • Treat the address as a wildcard iff addr.is_unspecified, or if it is an IPv4-mapped IPv6 address whose embedded IPv4 is unspecified.

The guard message now names the offending host so future bypass attempts are easier to spot in logs. Existing behavior is preserved: 0.0.0.0 is still blocked (the original test's match="0.0.0.0" still matches), and ::1, [::1], 127.0.0.1, localhost, specific LAN/public IPs, and ::ffff:127.0.0.1 are still permitted without --allow-public.

Validation

Updated packages/pickled-core/tests/test_mcp_transport.py:

  • Kept test_refuse_public_bind_without_flag and test_allow_public_bind for IPv4 regression coverage.
  • Added test_refuse_ipv6_and_padded_wildcards_without_flag parameterized over ::, [::], 0:0:0:0:0:0:0:0, ::ffff:0.0.0.0, and whitespace-padded forms — each must raise.
  • Added test_allow_ipv6_wildcards_with_flag — opt-in via --allow-public continues to work.
  • Added test_allow_loopback_and_specific_hosts_without_flag over 127.0.0.1, ::1, [::1], localhost, 192.168.1.10, ::ffff:127.0.0.1 — none of these are misidentified as wildcards.

Locally executed each case against the patched module to confirm both the new blocks and the unchanged allow paths.

Out of scope

  • Specific public/LAN IPs (e.g. 192.168.x.x) are intentionally left unguarded; matching the docstring, this PR only widens the existing "wildcard only" rule, it does not change which categories of hosts the guard targets.
  • No DNS resolution is performed; hostnames that happen to resolve to 0.0.0.0 are not blocked. Doing so would create its own surprise.
Open in Web View Automation 

The MCP HTTP transport refused to bind '0.0.0.0' without --allow-public
to prevent accidental public exposure, but only checked that one literal
string. Binding to '::' / '[::]' (the IPv6 wildcard, accepted by uvicorn
and FastMCP) silently exposed the MCP server on every IPv6 interface,
and on dual-stack hosts effectively on IPv4 as well. The safety check
gave a false sense of protection.

Replace the string-equality test with a structural unspecified-address
check via stdlib 'ipaddress', covering:

* the canonical and bracketed IPv6 wildcard ('::' / '[::]')
* the fully-expanded form ('0:0:0:0:0:0:0:0')
* the IPv4-mapped wildcard ('::ffff:0.0.0.0')
* trivial whitespace padding around any of the above
* the original '0.0.0.0' literal (regression coverage retained)

Loopback ('::1', '[::1]', '127.0.0.1'), localhost, specific LAN/public IPs,
and IPv4-mapped loopback ('::ffff:127.0.0.1') are intentionally left alone:
the guard only blocks wildcard binds, matching the original docstring.

All pickled-* MCP CLIs (pickled-bdd, pickled-rules, pickled-data,
pickled-iac, pickled-schema, pickled-diff) go through resolve_transport
and inherit the fix.

Co-authored-by: Bartłomiej Rosa <bartrosa@users.noreply.github.com>
@bartrosa
bartrosa marked this pull request as ready for review May 30, 2026 10:01
@bartrosa
bartrosa merged commit 078ccbf into main May 30, 2026
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants