Skip to content

Refuse a network rule the matcher would misread - #2619

Open
JspIIV wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
JspIIV:fix/network-rule-validation
Open

JspIIV wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
JspIIV:fix/network-rule-validation

Conversation

@JspIIV

@JspIIV JspIIV commented Sep 17, 2026

Copy link
Copy Markdown

Summary

Refuse a network-policy rule at config time when network_rule_matches would read it as something other than the glob it looks like.

The matcher runs every rule through urlsplit and then fnmatchcase. Those two disagree on three characters: urlsplit treats ? as the start of a query, # as a fragment, and [...] as an IPv6 literal, and it does so before fnmatch ever sees the rule. So on main:

rule what urlsplit leaves as the host what it matches
api?.example.com api a host literally named api, never api1.example.com
[a-z]*.com a-z (3.10) / ValueError (3.11+) a-z, or nothing
a#b.com a a host named a
https:// https a host named https

None of that raises. An egress allowlist with a typo in it narrows to a different destination, or to nothing, silently — and this is the policy the Docker proxy and the Prime/Modal providers enforce, so it is the wrong place to be quiet.

Change

check_network_rule runs from validate_network_policy over every allow and block rule (* exempt): ? and # are refused outright, a rule whose urlsplit raises or yields no host is refused with the parser's own message, and a bracketed rule is accepted only if the bracket holds an IPv6 address. Every rule that matched correctly before still validates: bare hosts, *.example.com, origins with schemes and ports, [::1]:8080.

with_task_network_policy goes through model_validate, so task-level rules get the same check.

Validation

No new tests, per AGENTS.md. Temporary script against this branch:

-- what the matcher does with these rules on main (unchanged) --
  api?.example.com     matches host 'api'                -> True
  api?.example.com     matches host 'api1.example.com'   -> False
  [a-z]*.com           matches host 'a-z'                -> False
  https://             matches host 'https'              -> True
  a#b.com              matches host 'a'                  -> True
-- validation --
  allow=['*']                        accepted
  allow=['example.com']              accepted
  allow=['*.example.com']            accepted
  allow=['https://example.com:8443'] accepted
  allow=['[::1]:8080']               accepted
  allow=['api?.example.com']         rejected: Value error, network rule 'api?.example.com': '?' and '#' start a URL query or fragment, not a glob
  allow=['[a-z]*.com']               rejected: Value error, network rule '[a-z]*.com': Invalid IPv6 URL
  allow=['a#b.com']                  rejected: Value error, network rule 'a#b.com': '?' and '#' start a URL query or fragment, not a glob
  allow=['https://']                 rejected: Value error, network rule 'https://': no host
  allow=['example.com:https']        rejected: Value error, network rule 'example.com:https': Port could not be cast to integer value as 'https'

uv run ruff check and uv run ruff format --check are clean on the touched file. The full pytest tests/ run does not collect on this machine (an unrelated anthropic.types.beta import in the environment here), and pre-commit's uv hook rejects the lockfile formatting under this uv version, so those two were not run locally.

Note

Reject malformed network rules in NetworkPolicyConfig validation

  • Adds runtime.check_network_rule to validate each rule before the matcher processes it. Accepts the unrestricted * rule; rejects ? and # delimiters, rules without a host, invalid ports, and bracketed hosts that are not valid IPv6 literals.
  • NetworkPolicyConfig.validate_network_policy now runs every entry in the normalized allow and block lists through check_network_rule, raising a rule-specific ValueError on failure.
  • Risk: existing configs with rules containing query/fragment delimiters, missing hosts, or bad ports will now fail validation at model-load time. Check allow and block entries in runtime.py for these patterns.

Macroscope summarized 58de16f.

network_rule_matches runs every rule through urlsplit before fnmatchcase,
and the two disagree on three characters. urlsplit reads '?' as a query,
'#' as a fragment and '[...]' as an IPv6 literal, so the rule
api?.example.com quietly becomes the host "api", [a-z]*.com becomes "a-z"
(or raises inside the matcher, which returns False), and https:// on its
own becomes the host "https". None of it raised: an egress allowlist with
a typo narrowed to a different destination, or to nothing, silently.

check_network_rule runs from the policy validator over every allow and
block rule. '?' and '#' are refused outright, a rule whose urlsplit raises
or yields no host is refused with the parser's own message, and brackets
are accepted only around an IPv6 address. Rules that already matched
correctly -- bare hosts, *.example.com, origins with schemes and ports,
[::1]:8080 -- validate as before. Task-level rules reach the same check
through with_task_network_policy's model_validate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant