Skip to content

fix(approval): match static-rule hosts case-insensitively - #41

Merged
bjhaid merged 2 commits into
brexhq:mainfrom
brian717:fix/host-case-insensitive-matching
Jul 27, 2026
Merged

fix(approval): match static-rule hosts case-insensitively#41
bjhaid merged 2 commits into
brexhq:mainfrom
brian717:fix/host-case-insensitive-matching

Conversation

@brian717

Copy link
Copy Markdown
Contributor

Problem

Host names are case-insensitive (RFC 4343), but staticURLMatches compares the URL and the rule pattern byte for byte. A rule written in lower case does not match a request that varies the case of the host, even though both reach the same server.

The effect shows up most often on allow rules. A rule for *.example.com does not match https://API.Example.com/v1/users, so traffic the operator already approved falls through to the LLM judge. That is a judge call on every such request, or a denial when the fallback is deny.

Deny rules have the same gap. It matters most when a broad allow rule is paired with a host-based deny carve-out, since a static allow returns before the judge runs.

Fix

Lower-case the scheme and host of both the URL and the pattern before matching. Only the authority is folded, so paths and query strings keep their case. Userinfo ahead of @ is left alone.

Doing this inside staticURLMatches covers prefix, exact and glob rules, and MatchesStaticRules in the eval runner picks it up as well.

Tests

TestStaticRuleHostCaseInsensitive covers upper and mixed case hosts against prefix, exact and glob rules for both actions, including a pattern authored in upper case matched against a lower-case request. It also asserts that a path differing only in case still does not match, so the fold stays limited to the authority.

Four rows added to TestStaticURLMatches for the same behaviour at the matcher level.

Ran go test ./internal/approval/ on Go 1.26.1 with the testcontainers Postgres setup. The new cases fail before the change and pass after it, and the rest of the package is unaffected.

Host names are case-insensitive (RFC 4343), but staticURLMatches compared
the URL and the rule pattern byte for byte. A rule written in lower case
did not match a request that varied the case of the host, even though both
reach the same server. An allow rule for *.example.com missed a request to
API.Example.com, which then fell through to the LLM judge instead of
matching the rule.

Fold the scheme and host of both the URL and the pattern to lower case
before matching. Only the authority is folded, so paths and query strings
stay case-sensitive. This covers prefix, exact and glob rules, and the
eval runner's MatchesStaticRules.
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes staticURLMatches to compare URL hosts case-insensitively, as required by RFC 4343, by introducing lowerAuthority which folds only the scheme and host to lowercase (preserving path, query, and userinfo). The fix is correctly ordered to run before stripDefaultPort, which was already case-sensitive on the scheme prefix — the previous review comment flagging this ordering issue is resolved by this change.

  • lowerAuthority locates the authority segment (handling schemeless patterns, IPv6, and userinfo containing @), lowercases scheme and host only, and is called before stripDefaultPort in staticURLMatches.
  • Two test files add comprehensive table-driven coverage: prefix/exact/glob rules for allow and deny, upper-case patterns against lower-case URLs, and two cases that explicitly exercise the lowerAuthority-before-stripDefaultPort ordering for HTTPS://host:443 patterns.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to case-folding only the scheme and host before matching, path and query semantics are unchanged, and the ordering concern raised in the previous review is directly addressed.

The implementation is correct: lowerAuthority isolates exactly the authority segment (handling schemeless patterns, userinfo with @, IPv6, and bare hostnames), and calling it before stripDefaultPort ensures uppercase-scheme patterns like HTTPS://host:443 have their redundant ports stripped. Test coverage is thorough — both the unit-level matcher tests and the higher-level MatchesStaticRules tests include negative cases (path case preserved) and the specific ordering scenario that was previously flagged.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
internal/approval/manager.go Adds lowerAuthority to fold scheme and host to lowercase before stripDefaultPort, then before pattern matching — correctly orders operations so uppercase-scheme patterns have their default ports stripped
internal/approval/hostcase_test.go New test file with 8 table-driven cases covering prefix/exact/glob rules for both allow and deny actions, plus an upper-case pattern against a lower-case URL, and a negative case confirming path case is preserved
internal/approval/manager_llm_test.go Adds 6 new rows to TestStaticURLMatches, including two specifically testing that an uppercase scheme with a redundant default port is normalised correctly — directly covering the ordering dependency between lowerAuthority and stripDefaultPort

Reviews (3): Last reviewed commit: "fix(approval): fold case before strippin..." | Re-trigger Greptile

stripDefaultPort matches the scheme with a case-sensitive HasPrefix, so a
pattern authored as "HTTPS://api.example.com:443/v1" kept its redundant
port while the request URL had it stripped, leaving the two strings
different and the rule unmatched.

Run lowerAuthority first so the scheme is already lower case by the time
stripDefaultPort inspects it.
@brian717

Copy link
Copy Markdown
Contributor Author

Good catch, confirmed. Swapped the order so the case fold runs first, and added table cases for HTTPS://host:443 and HTTP://host:80 so the ordering is pinned.

@bjhaid bjhaid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. it would be nice if we could do all of this work on load of StaticRule in a non-exported field normalizedPattern rather than on every request, probably a micro optimization but it is something that should count in the long run.

Happy to merge as it is and you can follow up if you want to address my concerns or I can find time to do it when I have some bandwidth.

@bjhaid
bjhaid merged commit 7cd8303 into brexhq:main Jul 27, 2026
4 checks passed
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.

2 participants