Skip to content

Permission rules targeting a URL silently never match — deny rules fail open #554

Description

@emal-avala

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions