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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
disable the recurring loop.

### Security
- 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
preventing subclass-controlled values from crossing immutable policy construction.
- Reject non-exact string subclasses in HTTP method policy values before
normalization or comma-separated parsing. Exact built-in strings and existing
comma-separated syntax remain supported, while runtime authorization rejects
subclass-controlled values before method normalization.
- Pin the credential-free verifier to a reviewed Python 3.13
`python@sha256:<64-hex>` digest, validate it before Docker execution, and
remove mutable-tag and `RepoDigests` promotion from the verifier boundary.
Expand Down
102 changes: 102 additions & 0 deletions docs/research/policy-configuration-integrity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Trusted policy configuration value integrity

## Decision

EgressWeave treats policy construction as a trusted startup boundary and stores a
canonical immutable policy value after normalization. Integer-form policy inputs
that become durable authority or resource-limit state therefore accept only an
exact built-in `int`. Non-exact integer subclasses are rejected instead of being
retained inside `EgressPolicy`.

The reviewed environment-configuration contract remains unchanged: ASCII decimal
strings are accepted where the corresponding public field already supports them
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.

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.

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`,
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.

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.

This supported-value sealing does not make EgressWeave a Python sandbox. Code
that is already executing inside the embedding process retains ordinary Python
capabilities. The boundary exists to make the documented policy value object
canonical, predictable, reviewable, and stable across standalone and modular
integrations.

## Enforcement invariants

1. Integer-form allowed ports must be exact built-in integers; reviewed ASCII
decimal strings are converted to built-in integers.
2. Integer-form DNS candidate limits must be exact built-in integers; reviewed
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
`bool` as an `int` subclass.
5. 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
field-specific `TypeError` or `ValueError` rather than becoming an opaque
request-time policy denial.
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

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 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*.
https://docs.python.org/3.14/reference/datamodel.html
10 changes: 5 additions & 5 deletions src/egressweave/_policy_normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def _normalize_allowed_port(value: object) -> int | None:
raise ValueError("allowed_ports entries must be decimal port numbers")
port = int(normalized)
else:
if isinstance(value, bool) or not isinstance(value, int):
if type(value) is not int:
raise TypeError("allowed_ports entries must be integer port numbers")
port = value

Expand Down 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 All @@ -188,7 +188,7 @@ def _normalize_max_resolved_addresses(value: object) -> int:
)
address_count = int(normalized)
else:
if isinstance(value, bool) or not isinstance(value, int):
if type(value) is not int:
raise TypeError("max_resolved_addresses must be an integer count")
address_count = value

Expand All @@ -205,7 +205,7 @@ def _normalize_positive_count(value: object, field_name: str) -> int:
raise ValueError(f"{field_name} must be a positive decimal count")
item_count = int(normalized)
else:
if isinstance(value, bool) or not isinstance(value, int):
if type(value) is not int:
raise TypeError(f"{field_name} must be an integer count")
item_count = value

Expand All @@ -224,7 +224,7 @@ def _normalize_positive_byte_count(value: object, field_name: str) -> int:
)
byte_count = int(normalized)
else:
if isinstance(value, bool) or not isinstance(value, int):
if type(value) is not int:
raise TypeError(f"{field_name} must be an integer byte count")
byte_count = value

Expand Down
13 changes: 10 additions & 3 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
125 changes: 125 additions & 0 deletions tests/test_policy_integer_value_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Security contracts for exact built-in integer policy values."""

from __future__ import annotations

from pathlib import Path

import pytest

from egressweave import EgressPolicy


class _PolicyIntegerSubclass(int):
"""Represent an unreviewed integer subclass crossing trusted configuration."""


def test_policy_rejects_integer_subclass_for_allowed_port() -> None:
"""Reject non-exact ports before retaining normalized authority state."""
with pytest.raises(TypeError, match="allowed_ports"):
EgressPolicy.from_hosts(
"api.example.com",
allowed_ports=[_PolicyIntegerSubclass(443)],
)


def test_policy_rejects_integer_subclass_for_exact_authority_port() -> None:
"""Reject a non-exact port in the exact-authority constructor as well."""
with pytest.raises(TypeError, match="allowed_ports"):
EgressPolicy.from_authorities(
[("api.example.com", _PolicyIntegerSubclass(443))],
)


@pytest.mark.parametrize(
"field_name",
[
"max_resolved_addresses",
"max_request_header_fields",
"max_response_header_fields",
"max_request_bytes",
"max_response_bytes",
"max_response_header_bytes",
"max_request_header_bytes",
"max_request_target_bytes",
],
)
def test_policy_rejects_integer_subclass_for_resource_limits(field_name: str) -> None:
"""Reject non-exact integers before retaining finite resource limits."""
with pytest.raises(TypeError, match=field_name):
EgressPolicy.from_hosts(
"api.example.com",
**{field_name: _PolicyIntegerSubclass(8)}, # type: ignore[arg-type]
)


def test_policy_keeps_exact_integer_and_decimal_string_configuration() -> None:
"""Preserve reviewed exact integers and decimal environment values."""
exact_integer = EgressPolicy.from_hosts(
"api.example.com",
allowed_ports=[8443],
max_resolved_addresses=8,
max_request_header_fields=32,
max_response_header_fields=32,
max_request_bytes=4096,
max_response_bytes=4096,
max_response_header_bytes=4096,
max_request_header_bytes=4096,
max_request_target_bytes=8192,
)
decimal_string = EgressPolicy.from_hosts(
"api.example.com",
allowed_ports=["8443"],
max_resolved_addresses="8",
max_request_header_fields="32",
max_response_header_fields="32",
max_request_bytes="4096",
max_response_bytes="4096",
max_response_header_bytes="4096",
max_request_header_bytes="4096",
max_request_target_bytes="8192",
)

assert decimal_string == exact_integer
assert all(type(port) is int for port in decimal_string.allowed_ports)
assert type(decimal_string.max_resolved_addresses) is int
assert type(decimal_string.max_request_header_fields) is int
assert type(decimal_string.max_response_header_fields) is int
assert type(decimal_string.max_request_bytes) is int
assert type(decimal_string.max_response_bytes) is int
assert type(decimal_string.max_response_header_bytes) is int
assert type(decimal_string.max_request_header_bytes) is int
assert type(decimal_string.max_request_target_bytes) is int


def test_exact_authority_keeps_integer_and_decimal_string_port_equivalent() -> None:
"""Preserve exact-authority ergonomics while storing canonical integer ports."""
exact_integer = EgressPolicy.from_authorities([("api.example.com", 8443)])
decimal_string = EgressPolicy.from_authorities([("api.example.com", "8443")])

assert decimal_string == exact_integer
assert decimal_string.allowed_authorities == frozenset(
{("api.example.com", 8443)}
)
assert all(type(port) is int for port in decimal_string.allowed_ports)
assert all(
type(port) is int for _, port in decimal_string.allowed_authorities
)


def test_policy_configuration_integrity_guide_is_discoverable_and_current() -> None:
"""Document the supported primitive-value boundary without sandbox claims."""
guide_path = Path("docs/research/policy-configuration-integrity.md")

assert guide_path.is_file()
guide = guide_path.read_text(encoding="utf-8")
assert "exact built-in `int`" in guide
assert "ASCII decimal strings" in guide
assert "does not make EgressWeave a Python sandbox" in guide
assert "https://docs.python.org/3.14/reference/datamodel.html" in guide


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

assert "Reject non-exact integer subclasses in shared policy integer fields" in changelog
Loading
Loading