-
Notifications
You must be signed in to change notification settings - Fork 0
security: reconstruct special-purpose IP policy on current main #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
c5d0142
test: reproduce special-purpose IP classification drift
seonghobae 48626ea
fix: stabilize special-purpose IP egress policy
seonghobae f88e8d9
docs: record special-purpose address compatibility policy
seonghobae 31e922a
docs: index special-purpose address policy
seonghobae f0920a9
docs: record special-purpose address hardening in release history
seonghobae d27ef15
test: bind special-purpose address policy documentation
seonghobae 015b026
test: keep special-purpose exceptions out of local authority
seonghobae a77772d
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae 45a0a4f
merge: update special-address policy on current main
seonghobae 76f512c
Merge branch 'main' of https://github.com/ContextualWisdomLab/EgressW…
seonghobae dbb6f98
test: keep special-address PR lint-clean
seonghobae 8065fc4
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae b75a030
test(security): cover mapped IPv4 registry compatibility
seonghobae 1fa903e
Merge branch 'main' into security/reconstruct-special-purpose-50a84d
opencode-agent[bot] 5f01348
fix: apply registry overlay to mapped IPv4 addresses
seonghobae 22a1b0d
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae 1b7be98
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae b8629e9
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae 2ce54e9
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae 7421130
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # Version-stable special-purpose address classification | ||
|
|
||
| EgressWeave supports more than one CPython minor and patch line. Security policy must therefore not silently widen or narrow merely because a deployment uses a runtime whose `ipaddress` module carries older special-purpose registry tables. Remote-address validation applies a small **reviewed compatibility overlay** to the parsed address value before consulting the running interpreter's version-sensitive classification properties. | ||
|
|
||
| This overlay is deliberately narrow. It freezes only reviewed differences that matter to EgressWeave's fail-closed remote-egress decision; it is not a private replacement for the IANA registries, and there is **no runtime registry download** or other network dependency in address validation. New or changed assignments require a normal source change, test-first evidence, current primary-source review, and the complete repository gate set. | ||
|
|
||
| ## Policy boundary | ||
|
|
||
| The compatibility layer preserves the current reviewed remote-egress classification for ranges whose behavior changed across otherwise supported interpreter versions. As revalidated against the live IANA registries and Python 3.14.6 documentation on 2026-08-11, the bounded set is: | ||
|
|
||
| | Address or range | EgressWeave remote decision | Current evidence | | ||
| |---|---|---| | ||
| | `192.0.0.0/24` | deny, except the two explicit globally reachable anycast addresses below | IANA IPv4 Special-Purpose Address Space and Python `ipaddress` | | ||
| | `192.0.0.9/32` | allow when every other EgressWeave authority check succeeds | IANA `Globally Reachable=True` exception | | ||
| | `192.0.0.10/32` | allow when every other EgressWeave authority check succeeds | IANA `Globally Reachable=True` exception | | ||
| | `64:ff9b:1::/48` | deny | IANA IPv6 Special-Purpose Address Space and Python `ipaddress` | | ||
| | `100:0:0:1::/64` | deny | IANA IPv6 Special-Purpose Address Space | | ||
| | `2001::/23` | deny except the reviewed globally reachable subranges below | IANA IPv6 Special-Purpose Address Space and Python `ipaddress` | | ||
| | `2001:1::1/128`, `2001:1::2/128`, `2001:1::3/128` | allow when every other EgressWeave authority check succeeds | IANA globally reachable exceptions; `2001:1::3/128` is explicit because the live registry is newer than the exception list currently described in Python documentation | | ||
| | `2001:3::/32`, `2001:4:112::/48`, `2001:20::/28`, `2001:30::/28` | allow when every other EgressWeave authority check succeeds | IANA globally reachable exceptions | | ||
| | `2001:2::/48` | deny | IANA IPv6 Special-Purpose Address Space | | ||
| | `2002::/16` | deny as the reviewed EgressWeave/CPython compatibility policy | Python 3.13+ classifies 6to4 as private; the IANA registry's `Globally Reachable` value is not a simple `False`, so this row must not be represented as an IANA-denial claim | | ||
| | `3fff::/20` | deny | IANA documentation prefix, not globally reachable | | ||
| | `5f00::/16` | deny | IANA Segment Routing SIDs prefix, not globally reachable | | ||
|
|
||
| The broad-network rule is evaluated after explicit globally reachable exceptions, so an exception cannot be swallowed by its parent `192.0.0.0/24` or `2001::/23` compatibility range. Outside the reviewed overlay, EgressWeave continues to apply the running interpreter's `is_private`, `is_loopback`, `is_link_local`, `is_reserved`, `is_unspecified`, `is_multicast`, and `is_global` properties as defense in depth. | ||
|
|
||
| `2002::/16` is intentionally called out because registry and library vocabularies are not perfectly interchangeable. Python 3.14.6 documents that Python 3.13 changed `2002::/16` to `is_private=True`. EgressWeave adopts that reviewed CPython fail-closed behavior for consistent supported-runtime decisions without making a stronger IANA claim than the live registry supports. | ||
|
|
||
| ## Local-development boundary | ||
|
|
||
| The compatibility overlay does not grant local authority. `allow_local=True` still requires the exact allowlisted local hostname and port and retains the existing loopback/private-container restrictions. IP-literal URLs remain forbidden. An address made globally acceptable by an explicit compatibility exception still has to pass every ordinary hostname, port, method, DNS-pinning, transport, TLS, request, and resource policy check. | ||
|
|
||
| ## Update procedure | ||
|
|
||
| A future registry or CPython change is a security-policy change, not a data refresh. Maintainers must: | ||
|
|
||
| 1. compare the live IANA IPv4 and IPv6 special-purpose registries with the supported Python documentation and relevant CPython change; | ||
| 2. add deterministic RED coverage that simulates the older interpreter property values while asserting the intended EgressWeave decision from the parsed address itself; | ||
| 3. add the smallest explicit range or exception required, with broader parent ranges evaluated only after specific global exceptions; | ||
| 4. update this note and release-facing history without overclaiming IANA semantics; and | ||
| 5. run the complete supported-Python matrix, 100% production statement/branch coverage, package acceptance, security scans, exact-head automated review, independent approval where required, and repository protections. | ||
|
|
||
| A mutable registry fetch at request time would make the security boundary non-reproducible and could change destination authority without a repository diff, so it remains out of scope. | ||
|
|
||
| ## References | ||
|
|
||
| Internet Assigned Numbers Authority. (2025, October 9). *IANA IPv4 Special-Purpose Address Space*. Retrieved August 11, 2026, from https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml | ||
|
|
||
| Internet Assigned Numbers Authority. (2025, October 9). *IANA IPv6 Special-Purpose Address Space*. Retrieved August 11, 2026, from https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml | ||
|
|
||
| Python Software Foundation. (2026). *ipaddress — IPv4/IPv6 manipulation library* (Python 3.14.6 documentation). https://docs.python.org/3.14/library/ipaddress.html | ||
|
|
||
| Stasiak, J. (2024, March 22). *GH-113171: Fix “private” (non-global) IP address ranges* [Commit `40d75c2`]. CPython. https://github.com/python/cpython/commit/40d75c2b7f5c67e254d0a025e0f2e2c7ada7f69f | ||
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| """Regression tests for version-stable special-purpose address classification.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import ipaddress | ||
|
|
||
| import pytest | ||
|
|
||
| from egressweave import EgressPolicy | ||
| from egressweave.validation import EgressNotAllowedError, _validate_global_address | ||
|
|
||
| _REMOTE_POLICY = EgressPolicy.from_hosts("api.example.com") | ||
|
|
||
|
|
||
| def _force_address_properties( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| address_type: type[ipaddress.IPv4Address | ipaddress.IPv6Address], | ||
| *, | ||
| private: bool, | ||
| global_: bool, | ||
| ) -> None: | ||
| """Replace version-sensitive stdlib address properties for one test.""" | ||
| monkeypatch.setattr(address_type, "is_private", property(lambda self: private)) | ||
| monkeypatch.setattr(address_type, "is_global", property(lambda self: global_)) | ||
| monkeypatch.setattr(address_type, "is_loopback", property(lambda self: False)) | ||
| monkeypatch.setattr(address_type, "is_link_local", property(lambda self: False)) | ||
| monkeypatch.setattr(address_type, "is_reserved", property(lambda self: False)) | ||
| monkeypatch.setattr(address_type, "is_unspecified", property(lambda self: False)) | ||
| monkeypatch.setattr(address_type, "is_multicast", property(lambda self: False)) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("address", "address_type"), | ||
| [ | ||
| ("192.0.0.8", ipaddress.IPv4Address), | ||
| ("64:ff9b:1::1", ipaddress.IPv6Address), | ||
| ("100:0:0:1::1", ipaddress.IPv6Address), | ||
| ("2001:2::1", ipaddress.IPv6Address), | ||
| ("2002::1", ipaddress.IPv6Address), | ||
| ("3fff::1", ipaddress.IPv6Address), | ||
| ("5f00::1", ipaddress.IPv6Address), | ||
| ], | ||
| ) | ||
| def test_reviewed_non_global_compatibility_ranges_remain_denied_when_stdlib_says_global( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| address: str, | ||
| address_type: type[ipaddress.IPv4Address | ipaddress.IPv6Address], | ||
| ) -> None: | ||
| """Deny reviewed compatibility ranges independently of stdlib patch data.""" | ||
| _force_address_properties( | ||
| monkeypatch, | ||
| address_type, | ||
| private=False, | ||
| global_=True, | ||
| ) | ||
|
|
||
| with pytest.raises(EgressNotAllowedError, match="egress URL is not allowed"): | ||
| _validate_global_address(address, _REMOTE_POLICY, hostname="api.example.com") | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("address", "address_type"), | ||
| [ | ||
| ("192.0.0.9", ipaddress.IPv4Address), | ||
| ("192.0.0.10", ipaddress.IPv4Address), | ||
| ("2001:1::1", ipaddress.IPv6Address), | ||
| ("2001:1::2", ipaddress.IPv6Address), | ||
| ("2001:1::3", ipaddress.IPv6Address), | ||
| ("2001:3::1", ipaddress.IPv6Address), | ||
| ("2001:4:112::1", ipaddress.IPv6Address), | ||
| ("2001:20::1", ipaddress.IPv6Address), | ||
| ("2001:30::1", ipaddress.IPv6Address), | ||
| ], | ||
| ) | ||
| def test_current_iana_global_exceptions_remain_allowed_when_stdlib_parent_is_private( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| address: str, | ||
| address_type: type[ipaddress.IPv4Address | ipaddress.IPv6Address], | ||
| ) -> None: | ||
| """Honor explicit IANA-global exceptions despite stale parent classification.""" | ||
| _force_address_properties( | ||
| monkeypatch, | ||
| address_type, | ||
| private=True, | ||
| global_=False, | ||
| ) | ||
|
|
||
| assert ( | ||
| _validate_global_address(address, _REMOTE_POLICY, hostname="api.example.com") | ||
| == address | ||
| ) | ||
|
|
||
|
|
||
| def test_ipv4_mapped_non_global_overlay_uses_underlying_ipv4_policy( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| ) -> None: | ||
| """Deny mapped special-purpose IPv4 even when stale IPv6 metadata says global.""" | ||
| _force_address_properties( | ||
| monkeypatch, | ||
| ipaddress.IPv6Address, | ||
| private=False, | ||
| global_=True, | ||
| ) | ||
|
|
||
| with pytest.raises(EgressNotAllowedError, match="egress URL is not allowed"): | ||
| _validate_global_address( | ||
| "::ffff:192.0.0.8", | ||
| _REMOTE_POLICY, | ||
| hostname="api.example.com", | ||
| ) | ||
|
|
||
|
|
||
| def test_ipv4_mapped_global_exception_uses_underlying_ipv4_policy( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| ) -> None: | ||
| """Allow mapped reviewed IPv4 exception despite stale IPv6 private metadata.""" | ||
| _force_address_properties( | ||
| monkeypatch, | ||
| ipaddress.IPv6Address, | ||
| private=True, | ||
| global_=False, | ||
| ) | ||
| address = "::ffff:192.0.0.9" | ||
|
|
||
| assert _validate_global_address( | ||
| address, | ||
| _REMOTE_POLICY, | ||
| hostname="api.example.com", | ||
| ) == str(ipaddress.ip_address(address)) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("address", ["192.0.0.9", "2001:1::1"]) | ||
| @pytest.mark.parametrize( | ||
| ("hostname", "policy"), | ||
| [ | ||
| ("localhost", EgressPolicy.from_hosts([], allow_local=True)), | ||
| ("ollama", EgressPolicy.from_hosts("ollama", allow_local=True)), | ||
| ], | ||
| ) | ||
| def test_global_compatibility_exceptions_do_not_widen_local_hostname_authority( | ||
| address: str, | ||
| hostname: str, | ||
| policy: EgressPolicy, | ||
| ) -> None: | ||
| """Keep local-development address scope narrower than remote compatibility.""" | ||
| with pytest.raises(EgressNotAllowedError, match="egress URL is not allowed"): | ||
| _validate_global_address(address, policy, hostname=hostname) |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| """Contracts keeping special-purpose address policy and shipped guidance aligned.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[1] | ||
| GUIDE = ROOT / "docs" / "research" / "special-purpose-address-classification.md" | ||
| RESEARCH_INDEX = ROOT / "docs" / "research" / "README.md" | ||
| CHANGELOG = ROOT / "CHANGELOG.md" | ||
| VALIDATION = ROOT / "src" / "egressweave" / "validation.py" | ||
|
|
||
|
|
||
| def test_special_purpose_policy_has_discoverable_current_primary_source_guidance() -> None: | ||
| """Keep the compatibility policy discoverable and grounded in current sources.""" | ||
| guide = GUIDE.read_text(encoding="utf-8") | ||
| index = RESEARCH_INDEX.read_text(encoding="utf-8") | ||
|
|
||
| for fragment in ( | ||
| "Version-stable special-purpose address classification", | ||
| "no runtime registry download", | ||
| "IANA IPv4 Special-Purpose Address Space", | ||
| "IANA IPv6 Special-Purpose Address Space", | ||
| "Python 3.14.6", | ||
| "40d75c2b7f5c67e254d0a025e0f2e2c7ada7f69f", | ||
| "2001:1::3/128", | ||
| "2002::/16", | ||
| ): | ||
| assert fragment in guide | ||
|
|
||
| assert "special-purpose-address-classification.md" in index | ||
|
|
||
|
|
||
| def test_release_history_records_version_stable_address_hardening() -> None: | ||
| """Keep buyer-facing unreleased history aligned with the security change.""" | ||
| changelog = CHANGELOG.read_text(encoding="utf-8") | ||
|
|
||
| assert "Stabilize special-purpose remote-address classification" in changelog | ||
| assert "no runtime registry download" in changelog | ||
|
|
||
|
|
||
| def test_documented_compatibility_ranges_match_source_controlled_overlay() -> None: | ||
| """Prevent silent drift between the reviewed guide and implementation constants.""" | ||
| guide = GUIDE.read_text(encoding="utf-8") | ||
| source = VALIDATION.read_text(encoding="utf-8") | ||
|
|
||
| for network in ( | ||
| "192.0.0.0/24", | ||
| "192.0.0.9/32", | ||
| "192.0.0.10/32", | ||
| "64:ff9b:1::/48", | ||
| "100:0:0:1::/64", | ||
| "2001::/23", | ||
| "2001:1::1/128", | ||
| "2001:1::2/128", | ||
| "2001:1::3/128", | ||
| "2001:3::/32", | ||
| "2001:4:112::/48", | ||
| "2001:20::/28", | ||
| "2001:30::/28", | ||
| "2002::/16", | ||
| "3fff::/20", | ||
| "5f00::/16", | ||
| ): | ||
| assert network in guide | ||
| assert network in source |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.