fix(pickled-core): close IPv6 wildcard bypass of MCP --allow-public guard - #27
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug and impact
pickled_core.mcp.transport.resolve_transportis the single chokepoint everypickled-*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:That misses the IPv6 wildcard. Running, for example,
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 ofpackages/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-expanded0: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:[::]→::).ipaddress.ip_address. Non-IP literals (hostnames,localhost) fall through unchecked — DNS resolution would be too dynamic for this guard.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.0is still blocked (the original test'smatch="0.0.0.0"still matches), and::1,[::1],127.0.0.1,localhost, specific LAN/public IPs, and::ffff:127.0.0.1are still permitted without--allow-public.Validation
Updated
packages/pickled-core/tests/test_mcp_transport.py:test_refuse_public_bind_without_flagandtest_allow_public_bindfor IPv4 regression coverage.test_refuse_ipv6_and_padded_wildcards_without_flagparameterized over::,[::],0:0:0:0:0:0:0:0,::ffff:0.0.0.0, and whitespace-padded forms — each must raise.test_allow_ipv6_wildcards_with_flag— opt-in via--allow-publiccontinues to work.test_allow_loopback_and_specific_hosts_without_flagover127.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
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.0.0.0.0are not blocked. Doing so would create its own surprise.