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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
without changing the centrally managed review-agent credential contract.

### Security
- Reject non-exact string subclasses in HTTP method policy values before
normalization or comma-separated parsing, preserving ordinary built-in method
strings, RFC 9110 token validation, uppercase canonicalization, and `CONNECT` denial.
- Reject non-exact integer subclasses in shared policy integer fields before
retaining trusted configuration state. Exact built-in integers and existing
ASCII decimal strings remain supported, preserving defaults and ranges while
Expand Down
74 changes: 49 additions & 25 deletions docs/research/policy-configuration-integrity.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,39 @@ and are converted to exact built-in integers before range checks, relational
checks, policy fingerprinting, DNS validation, request validation, or transport
delegation. Exact built-in integers continue to be accepted directly.

HTTP method policy values use the same supported-value sealing principle. Each
individual method token must be an exact built-in `str` before whitespace removal
or uppercase normalization can run. The existing exact comma-separated
`allowed_methods` string remains supported and is split only after its outer value
has been proven to be an exact built-in string. The resulting method entries still
follow the RFC 9110 token grammar, are canonicalized to uppercase, and always
reject `CONNECT`.

This restriction applies to the shared integer normalization paths for allowed
ports, maximum resolved-address count, positive header-field counts, and positive
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.
request/response byte budgets, plus the reviewed HTTP method normalization paths.
It does not change configured defaults, allowed ranges, authority pairing, DNS
policy, TLS identity, proxy isolation, request/response framing, or the generic
request-time denial boundary.

## Why exact type matters at this boundary

Python deliberately supports subclassing immutable built-in types such as `int`,
and `isinstance(value, int)` is true for instances of subclasses. Python's data
model also permits subclasses of immutable built-ins to customize instance
creation. A broad `isinstance` check is therefore a polymorphism contract, not
proof that the stored object is the canonical built-in integer value expected by
a closed immutable policy representation.
Python deliberately supports subclassing immutable built-in types such as `int`
and `str`. `isinstance(value, int)` or `isinstance(value, str)` therefore accepts
subclass instances, while Python's data model permits immutable built-in
subclasses to customize behavior. A broad `isinstance` check is consequently a
polymorphism contract, not proof that the stored or parsed value is the canonical
built-in primitive expected by a closed immutable policy representation.

EgressWeave does not need that polymorphism for policy scalar fields. Supported
customization is expressed through documented values, not user-defined numeric
classes. Requiring `type(value) is int` on integer-form inputs prevents a subclass
object from surviving normalization and later participating in policy hashing or
equality, authority tuples, arithmetic or comparison boundaries, or provider
delegation. Environment text still reaches the same canonical state through
explicit decimal conversion.
customization is expressed through documented values, not user-defined numeric or
string classes. Requiring `type(value) is int` on integer-form inputs prevents a
subclass object from surviving normalization and later participating in policy
hashing or equality, authority tuples, arithmetic or comparison boundaries, or
provider delegation. Requiring `type(value) is str` for HTTP methods prevents a
subclass from controlling `strip()`, `upper()`, or the comma-separated `split()`
step before trusted normalization. Environment text still reaches the same
canonical state through explicit decimal conversion or ordinary built-in strings.

This supported-value sealing does not make EgressWeave a Python sandbox. Code
that is already executing inside the embedding process retains ordinary Python
Expand All @@ -51,26 +62,39 @@ integrations.
ASCII decimal strings are converted before positivity checks.
3. Shared positive field-count and byte-budget normalizers reject integer
subclasses and preserve their existing positive-value constraints.
4. Booleans remain invalid integer configuration even though Python defines
4. HTTP method entries must be exact built-in strings before whitespace removal
or uppercase normalization; non-exact `str` subclasses are rejected.
5. The exact built-in comma-separated `allowed_methods` form remains supported,
but a string subclass is rejected before `split()` can run. RFC 9110 token
validation, uppercase canonicalization, and unconditional `CONNECT` rejection
remain unchanged.
6. Booleans remain invalid integer configuration even though Python defines
`bool` as an `int` subclass.
5. Existing decimal-string syntax, defaults, public builder signatures, and
7. Existing decimal-string syntax, defaults, public builder signatures, and
request-time generic denial behavior remain unchanged.
6. Invalid trusted startup configuration continues to raise actionable
8. Invalid trusted startup configuration continues to raise actionable
field-specific `TypeError` or `ValueError` rather than becoming an opaque
request-time policy denial.
7. Regression tests exercise the public `EgressPolicy` constructors so the
9. Regression tests exercise the public `EgressPolicy` constructors so the
contract is proven at the API boundary rather than only against internal
helpers.

## Operator migration

Applications that supply plain integers or ASCII decimal environment values need
no change. Applications that pass custom subclasses of `int` for ports or finite
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 plain integers, ASCII decimal environment values, or
ordinary built-in HTTP method strings need no change. The existing exact
comma-separated `allowed_methods` string is also unchanged. Applications that
pass custom subclasses of `int` for ports or finite resource budgets should
materialize an exact built-in integer before policy construction; applications
that pass custom subclasses of `str` for HTTP methods should materialize an
ordinary built-in string first. This is a pre-1.0 tightening of ambiguous
configuration shapes; it does not widen egress authority or change any finite
default.

## References — APA 7th

## Reference — APA 7th
Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110;
STD 97). RFC Editor. https://www.rfc-editor.org/rfc/rfc9110.html

Python Software Foundation. (2026). *Data model — Python 3.14.6 documentation*.
https://docs.python.org/3.14/reference/datamodel.html
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
6 changes: 6 additions & 0 deletions src/egressweave/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ def __post_init__(self) -> None:

method_values: Iterable[object]
if isinstance(self.allowed_methods, str):
if type(self.allowed_methods) is not str:
raise TypeError("allowed_methods entries must be HTTP method strings")
method_values = self.allowed_methods.split(",")
else:
method_values = self.allowed_methods
Expand Down Expand Up @@ -371,6 +373,8 @@ def from_hosts(

method_items: Iterable[str]
if isinstance(allowed_methods, str):
if type(allowed_methods) is not str:
raise TypeError("allowed_methods entries must be HTTP method strings")
method_items = allowed_methods.split(",")
else:
method_items = allowed_methods
Expand Down Expand Up @@ -434,6 +438,8 @@ def from_authorities(
)
method_items: Iterable[str]
if isinstance(allowed_methods, str):
if type(allowed_methods) is not str:
raise TypeError("allowed_methods entries must be HTTP method strings")
method_items = allowed_methods.split(",")
else:
method_items = allowed_methods
Expand Down
96 changes: 96 additions & 0 deletions tests/test_policy_method_value_integrity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""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 unsafe 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 entries must be HTTP method strings$",
):
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 entries must be HTTP method strings$",
):
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 entries must be HTTP method strings$",
):
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 entries must be HTTP method strings$",
):
EgressPolicy.from_authorities(
[("api.example.com", 443)],
allowed_methods=_ExplodingMethodList("GET,POST"),
)


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