Skip to content
Closed
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
retaining trusted configuration state. Exact built-in integers and existing
ASCII decimal strings remain supported, preserving defaults and ranges while
preventing subclass-controlled values from crossing immutable policy construction.
- Reject non-exact string subclasses in HTTP method policy values before
normalization, comma-separated parsing, or runtime authorization.
- Erase private request-method normalization exception provenance from
caller-visible policy denials.
- Pin the credential-free verifier to a reviewed Python 3.13
Expand Down
26 changes: 26 additions & 0 deletions docs/research/policy-configuration-integrity.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ request/response byte budgets. It does not change configured defaults, allowed
ranges, authority pairing, DNS policy, TLS identity, proxy isolation, HTTP method
policy, request/response framing, or the generic request-time denial boundary.

HTTP method policy values are sealed at the same trusted startup boundary. Each
method value must be an exact built-in `str` before trimming, uppercase
canonicalization, or RFC 9110 token validation can invoke string behavior.
Supported comma-separated operator syntax remains available only when the outer
configuration value itself is an exact built-in `str`; non-exact string
subclasses are rejected before `split()` can run. Runtime method authorization
uses the same exact-string boundary and returns the existing generic denial for
unsupported caller values.

This method-value restriction preserves the documented default and deny-all sets,
ordinary exact strings, comma-separated ergonomics, uppercase canonicalization,
RFC 9110 token validation, and unconditional `CONNECT` denial. It does not make
EgressWeave a Python sandbox: arbitrary trusted Python already executing in the
embedding process retains ordinary Python capabilities.

## Why exact type matters at this boundary

Python deliberately supports subclassing immutable built-in types such as `int`,
Expand Down Expand Up @@ -61,6 +76,11 @@ integrations.
7. Regression tests exercise the public `EgressPolicy` constructors so the
contract is proven at the API boundary rather than only against internal
helpers.
8. HTTP method entries and supported comma-separated method configuration must
be exact built-in strings before any subclass-controllable normalization or
splitting operation.
9. Runtime HTTP method authorization rejects non-exact string subclasses without
invoking their normalization methods.

## Operator migration

Expand All @@ -70,6 +90,12 @@ resource budgets should materialize an exact built-in integer before policy
construction. This is a pre-1.0 tightening of an ambiguous configuration shape;
it does not widen egress authority or change any finite default.

Applications that supply ordinary method strings or the existing comma-separated
method syntax also need no change. Integrations that pass subclasses of `str` for
method configuration should materialize exact built-in strings before policy
construction. This is likewise a supported-value tightening, not an expansion of
HTTP authority.

## Reference — APA 7th

Python Software Foundation. (2026). *Data model — Python 3.14.6 documentation*.
Expand Down
2 changes: 1 addition & 1 deletion src/egressweave/_policy_normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def _normalize_allowed_method(value: object) -> str:
is never accepted: its semantics create an application-layer tunnel whose
destination is independent of the validated URL authority.
"""
if not isinstance(value, str):
if type(value) is not str:
raise TypeError("allowed_methods entries must be HTTP method strings")

normalized = value.strip().upper()
Expand Down
15 changes: 11 additions & 4 deletions src/egressweave/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@
)


def _split_exact_method_string(value: object) -> list[str]:
"""Split a comma-separated method list only after exact-string validation."""
if type(value) is not str:
raise TypeError("allowed_methods must use exact built-in strings")
return value.split(",")


@dataclass(frozen=True)
class EgressPolicy:
"""Immutable outbound-egress allowlist and resource policy.
Expand Down Expand Up @@ -244,7 +251,7 @@ def __post_init__(self) -> None:

method_values: Iterable[object]
if isinstance(self.allowed_methods, str):
method_values = self.allowed_methods.split(",")
method_values = _split_exact_method_string(self.allowed_methods)
else:
method_values = self.allowed_methods
normalized_methods = frozenset(
Expand Down Expand Up @@ -371,7 +378,7 @@ def from_hosts(

method_items: Iterable[str]
if isinstance(allowed_methods, str):
method_items = allowed_methods.split(",")
method_items = _split_exact_method_string(allowed_methods)
else:
method_items = allowed_methods

Expand Down Expand Up @@ -434,7 +441,7 @@ def from_authorities(
)
method_items: Iterable[str]
if isinstance(allowed_methods, str):
method_items = allowed_methods.split(",")
method_items = _split_exact_method_string(allowed_methods)
else:
method_items = allowed_methods

Expand Down Expand Up @@ -504,4 +511,4 @@ def allows_http_method(self, method: str) -> bool:
normalized = _normalize_allowed_method(method)
except (TypeError, ValueError):
return False
return normalized in self.allowed_methods
return normalized in self.allowed_methods
94 changes: 94 additions & 0 deletions tests/test_policy_method_value_integrity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Regression tests for exact built-in HTTP method policy values."""

from __future__ import annotations

from pathlib import Path

import pytest

from egressweave.policy import EgressPolicy


class _NonExactMethod(str):
"""Keep subclass identity if trusted normalization invokes polymorphic methods."""

def strip(self, chars: str | None = None) -> _NonExactMethod:
"""Return this subclass instead of a canonical built-in string."""
return self

def upper(self) -> _NonExactMethod:
"""Return this subclass instead of a canonical built-in string."""
return self


class _ExplodingMethodList(str):
"""Expose polymorphic dispatch in comma-separated method parsing."""

def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]:
"""Fail if trusted construction invokes subclass-controlled splitting."""
raise AssertionError("string subclass split executed")


def test_method_policy_rejects_str_subclass_before_normalization() -> None:
"""Reject subclass-controlled method normalization during policy construction."""
with pytest.raises(TypeError, match="allowed_methods"):
EgressPolicy.from_hosts(
"api.example.com",
allowed_methods={_NonExactMethod("GET")},
)


def test_direct_policy_rejects_str_subclass_before_comma_split() -> None:
"""Reject a direct comma-string subclass before invoking its split method."""
with pytest.raises(TypeError, match="allowed_methods"):
EgressPolicy(
allowed_hosts=frozenset({"api.example.com"}),
allowed_methods=_ExplodingMethodList("GET,POST"),
)


def test_from_hosts_rejects_str_subclass_before_comma_split() -> None:
"""Reject a host-factory comma-string subclass before invoking split."""
with pytest.raises(TypeError, match="allowed_methods"):
EgressPolicy.from_hosts(
"api.example.com",
allowed_methods=_ExplodingMethodList("GET,POST"),
)


def test_from_authorities_rejects_str_subclass_before_comma_split() -> None:
"""Reject an authority-factory comma-string subclass before invoking split."""
with pytest.raises(TypeError, match="allowed_methods"):
EgressPolicy.from_authorities(
[("api.example.com", 443)],
allowed_methods=_ExplodingMethodList("GET,POST"),
)


def test_runtime_method_authorization_rejects_str_subclass_before_normalization() -> None:
"""Reject subclass-controlled normalization at the request authorization boundary."""
policy = EgressPolicy.from_hosts(
"api.example.com",
allowed_methods={"GET"},
)

assert policy.allows_http_method(_NonExactMethod("GET")) is False


def test_policy_configuration_integrity_guide_covers_exact_method_strings() -> None:
"""Document the exact HTTP method value boundary and preserved string syntax."""
guide = Path("docs/research/policy-configuration-integrity.md").read_text(
encoding="utf-8"
)

assert "exact built-in `str`" in guide
assert "HTTP method" in guide
assert "comma-separated" in guide
assert "does not make EgressWeave a Python sandbox" in guide


def test_changelog_records_http_method_value_sealing() -> None:
"""Record the method-string policy tightening in release history."""
changelog = Path("CHANGELOG.md").read_text(encoding="utf-8")

assert "Reject non-exact string subclasses in HTTP method policy values" in changelog
Loading