Found during a systematic audit of the TUI gap register against origin/main (v0.29.0). This one fails open, which is why it is filed as a bug rather than a gap.
A permission rule whose pattern targets a URL can never match
matches_input_pattern (crates/lib/src/permissions/mod.rs:455-482) resolves the haystack like this:
if let Some(command) = input.get("command")... { /* Bash / other */ }
let input_str = input
.get("file_path")
.or_else(|| input.get("pattern"))
.and_then(|v| v.as_str())
.unwrap_or(""); // <-- everything else lands here
glob_match(pattern, input_str)
It inspects command, file_path, pattern — and nothing else. WebFetch input carries a url field, so it falls through to glob_match(pattern, "").
glob_match against an empty haystack cannot match any pattern with a literal character in it (glob_match_inner, :561 — a Some(literal) vs None pair returns false).
Consequence
A configured rule like:
[[permissions.rules]]
tool = "WebFetch"
pattern = "*.internal.example.com"
action = "deny"
silently does nothing. It parses, it loads, it is present in the rule set, it is evaluated — and it never matches, so the request falls through to whatever the default mode is. The operator sees a rule in their config and gets no protection from it.
The allow direction is equally broken but fails safe (the rule simply never grants). The deny direction fails open, which is the problem.
Not limited to WebFetch
Any tool whose input key is not command / file_path / pattern has the same behaviour. That is the general form of the bug: the field list is an allowlist of three names with a silent empty-string fallback.
Suggested fix
- Add
url to the fallback chain (cheapest fix, closes the reported case).
- Better: when none of the known keys are present, do not silently match against
"". Either match against a canonical serialization of the input, or treat "no comparable field" as a hard non-match for allow and a hard match for deny — failing closed rather than open.
- A test per tool kind asserting that a
deny rule with a pattern actually denies. The existing tests cover Bash and file paths, which is why this survived.
Related
Surfaced while verifying gap-register rows D9-13 (network approval as a first-class request kind) and D9-16 (per-MCP-tool grants); both are separate and remain open. This is the narrow, immediately-exploitable piece.
Found during a systematic audit of the TUI gap register against
origin/main(v0.29.0). This one fails open, which is why it is filed as a bug rather than a gap.A permission rule whose pattern targets a URL can never match
matches_input_pattern(crates/lib/src/permissions/mod.rs:455-482) resolves the haystack like this:It inspects
command,file_path,pattern— and nothing else.WebFetchinput carries aurlfield, so it falls through toglob_match(pattern, "").glob_matchagainst an empty haystack cannot match any pattern with a literal character in it (glob_match_inner,:561— aSome(literal)vsNonepair returns false).Consequence
A configured rule like:
silently does nothing. It parses, it loads, it is present in the rule set, it is evaluated — and it never matches, so the request falls through to whatever the default mode is. The operator sees a rule in their config and gets no protection from it.
The allow direction is equally broken but fails safe (the rule simply never grants). The deny direction fails open, which is the problem.
Not limited to WebFetch
Any tool whose input key is not
command/file_path/patternhas the same behaviour. That is the general form of the bug: the field list is an allowlist of three names with a silent empty-string fallback.Suggested fix
urlto the fallback chain (cheapest fix, closes the reported case)."". Either match against a canonical serialization of the input, or treat "no comparable field" as a hard non-match forallowand a hard match fordeny— failing closed rather than open.denyrule with a pattern actually denies. The existing tests cover Bash and file paths, which is why this survived.Related
Surfaced while verifying gap-register rows D9-13 (network approval as a first-class request kind) and D9-16 (per-MCP-tool grants); both are separate and remain open. This is the narrow, immediately-exploitable piece.