From 15b1ecee9cd77d5fde9aae409a7f22f956ca7229 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 09:18:51 +0900 Subject: [PATCH 1/9] test: reproduce method-string subclass dispatch --- tests/test_policy_method_value_integrity.py | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_policy_method_value_integrity.py diff --git a/tests/test_policy_method_value_integrity.py b/tests/test_policy_method_value_integrity.py new file mode 100644 index 00000000..9bcb6eee --- /dev/null +++ b/tests/test_policy_method_value_integrity.py @@ -0,0 +1,63 @@ +"""Regression tests for exact built-in HTTP method policy values.""" + +from __future__ import annotations + +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"), + ) From 7cb22cc94c7407a97b33ea1330d22e80f9b63a9e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 09:22:56 +0900 Subject: [PATCH 2/9] test: keep method-string RED lint-clean --- tests/test_policy_method_value_integrity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_policy_method_value_integrity.py b/tests/test_policy_method_value_integrity.py index 9bcb6eee..b4c12912 100644 --- a/tests/test_policy_method_value_integrity.py +++ b/tests/test_policy_method_value_integrity.py @@ -10,11 +10,11 @@ class _NonExactMethod(str): """Keep subclass identity if trusted normalization invokes polymorphic methods.""" - def strip(self, chars: str | None = None) -> "_NonExactMethod": + def strip(self, chars: str | None = None) -> _NonExactMethod: """Return this subclass instead of a canonical built-in string.""" return self - def upper(self) -> "_NonExactMethod": + def upper(self) -> _NonExactMethod: """Return this subclass instead of a canonical built-in string.""" return self From 654c7c2ff4eb7c0f2c8dc52628119cddc606bf77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:08:30 +0900 Subject: [PATCH 3/9] security: require exact HTTP method policy strings --- src/egressweave/policy.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/egressweave/policy.py b/src/egressweave/policy.py index 1c69a1ff..547dcf1f 100644 --- a/src/egressweave/policy.py +++ b/src/egressweave/policy.py @@ -69,6 +69,13 @@ ) +def _require_exact_method_string(value: object) -> str: + """Return a built-in HTTP method string without subclass dispatch.""" + if type(value) is not str: + raise TypeError("allowed_methods must use exact built-in strings") + return value + + @dataclass(frozen=True) class EgressPolicy: """Immutable outbound-egress allowlist and resource policy. @@ -244,11 +251,12 @@ def __post_init__(self) -> None: method_values: Iterable[object] if isinstance(self.allowed_methods, str): - method_values = self.allowed_methods.split(",") + method_values = _require_exact_method_string(self.allowed_methods).split(",") else: method_values = self.allowed_methods normalized_methods = frozenset( - _normalize_allowed_method(method) for method in method_values + _normalize_allowed_method(_require_exact_method_string(method)) + for method in method_values ) normalized_max_resolved_addresses = _normalize_max_resolved_addresses( self.max_resolved_addresses @@ -371,7 +379,7 @@ def from_hosts( method_items: Iterable[str] if isinstance(allowed_methods, str): - method_items = allowed_methods.split(",") + method_items = _require_exact_method_string(allowed_methods).split(",") else: method_items = allowed_methods @@ -434,7 +442,7 @@ def from_authorities( ) method_items: Iterable[str] if isinstance(allowed_methods, str): - method_items = allowed_methods.split(",") + method_items = _require_exact_method_string(allowed_methods).split(",") else: method_items = allowed_methods From 821d09b48267dc8d3adbd7ec7bc30882d79ce5eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:10:14 +0900 Subject: [PATCH 4/9] test: expose runtime HTTP method subclass authorization --- tests/test_policy_method_value_integrity.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_policy_method_value_integrity.py b/tests/test_policy_method_value_integrity.py index b4c12912..f3a03d18 100644 --- a/tests/test_policy_method_value_integrity.py +++ b/tests/test_policy_method_value_integrity.py @@ -61,3 +61,13 @@ def test_from_authorities_rejects_str_subclass_before_comma_split() -> None: [("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 From c0128bf38a437616b867e0408a731216a7318431 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:11:48 +0900 Subject: [PATCH 5/9] security: seal runtime HTTP method normalization --- src/egressweave/_policy_normalization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/egressweave/_policy_normalization.py b/src/egressweave/_policy_normalization.py index 0a190a5d..52ecdb86 100644 --- a/src/egressweave/_policy_normalization.py +++ b/src/egressweave/_policy_normalization.py @@ -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() From 02689e294b06846a05426fb5827f8bffde8bb6bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:12:59 +0900 Subject: [PATCH 6/9] test: require method policy documentation parity --- tests/test_policy_method_value_integrity.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_policy_method_value_integrity.py b/tests/test_policy_method_value_integrity.py index f3a03d18..c80c8ee3 100644 --- a/tests/test_policy_method_value_integrity.py +++ b/tests/test_policy_method_value_integrity.py @@ -2,6 +2,8 @@ from __future__ import annotations +from pathlib import Path + import pytest from egressweave.policy import EgressPolicy @@ -71,3 +73,22 @@ def test_runtime_method_authorization_rejects_str_subclass_before_normalization( ) 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 From 048d856d8626249dd55974cc0757e448e150260f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:15:25 +0900 Subject: [PATCH 7/9] docs: define exact HTTP method value integrity --- .../policy-configuration-integrity.md | 74 ++++++++++++------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/docs/research/policy-configuration-integrity.md b/docs/research/policy-configuration-integrity.md index ea35b13f..35b6a1d9 100644 --- a/docs/research/policy-configuration-integrity.md +++ b/docs/research/policy-configuration-integrity.md @@ -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 @@ -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 From 25dcb848d879061027d433a168e30d8efacc796a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:17:10 +0900 Subject: [PATCH 8/9] docs: record HTTP method policy sealing --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2eef161..80338033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -365,4 +368,4 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). gap (CWE-350), with redirects and environment proxies disabled. - `EgressNotAllowedError` (a `ValueError` subclass) and `ValidatedEgressURL`. - 35 tests covering URL rejection, address classification, the `allow_local` - container case, DNS-to-private rejection, and transport pinning. + container case, DNS-to-private rejection, and transport pinning. \ No newline at end of file From 721f6ce10035a0e7caded2eb8417cdc6894c7e70 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 10:19:00 +0900 Subject: [PATCH 9/9] docs: preserve changelog newline --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80338033..7bf94677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -368,4 +368,4 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). gap (CWE-350), with redirects and environment proxies disabled. - `EgressNotAllowedError` (a `ValueError` subclass) and `ValidatedEgressURL`. - 35 tests covering URL rejection, address classification, the `allow_local` - container case, DNS-to-private rejection, and transport pinning. \ No newline at end of file + container case, DNS-to-private rejection, and transport pinning.