Repository navigation
Permission deny rules fail open for argument values containing / (matchGlob uses filepath.Match) #3604
Description
Activity
- addedarea/securityAuthentication, authorization, secrets, vulnerabilitiesAuthentication, authorization, secrets, vulnerabilities
on Jul 11, 2026 Suggested direction: for argument-value matching, use a matcher whose
*spans any character including separators - translate the glob to an anchored, case-insensitive regexp, or usedoublestarwith separator matching disabled - instead offilepath.Match, and add test covering non-trailing wildcards against values with/(bothdenyandallow).Happy to open a PR with the fix + tests 🚀
Command arguments are not filesystem paths, so filepath.Match brings separator semantics that are surprising and platform-dependent here. A separator-agnostic matcher would fix this specific bypass, but typed parsing is safer: identify the executable and structured arguments first, then apply policy to those fields. Please also add an invariant test that a deny rule can never degrade into ask or allow under yolo mode, including slash, backslash, quoting, and chained-command cases.
Command arguments are not filesystem paths, so filepath.Match brings separator semantics that are surprising and platform-dependent here. A separator-agnostic matcher would fix this specific bypass, but typed parsing is safer: identify the executable and structured arguments first, then apply policy to those fields. Please also add an invariant test that a deny rule can never degrade into ask or allow under yolo mode, including slash, backslash, quoting, and chained-command cases.
Agreed on the framing, command args aren't paths, and inheriting
filepath.Match's separator semantics for them is the bug.Typed parsing I'd say is right long-term but not a substitute here: you'd still glob-match policy against the parsed fields, and those fields are paths and URLs, so they're still full of
/- it relocates this bug rather than removing it. It also reinterprets every existing
shell:cmd=<glob>rule, which feels like it wants its own design issue rather than riding along on a fail-open fix.shell_grant.goalready does the strict reading for safety overrides; widening that is the real proposal. Happy to open an issue if you want it tracked.On the yolo invariant, I can't write that test, because
Decideshort-circuits on yolo before any checker runs, andTestDecide_YoloShortCircuitsalready pins that down with a deny rule in place. Under--yolodeny isn't degraded, it's never consulted. Whether it should outrank yolo is a fair question (I'd say yes - yolo reads as "stop asking me", not "ignore my denylist"), but that's a behavior change well outside this PR; happy to raise it separately if you think it's worth having :)I did take the matrix and apply it where the invariant does hold, at the checker: deny now has to beat an
overlapping allow and ask across slashes, backslashes, quoting, chaining and$( )👍
Description
A configured
denypermission whose argument value contains a path separator (/, or\on Windows)can silently fail to match, so a tool call the operator explicitly tried to block is not denied.
The matcher
matchGlobinpkg/permissions/permissions.gohas two paths:*fast path: if the pattern ends in*and the prefix contains no other globmetacharacters, it does a plain
strings.HasPrefix. This is whyshell:cmd=rm*andshell:cmd=sudo*correctly block
rm -rf /.filepath.Match(pattern, value).filepath.Match's*matchesany run of non-separator characters, so it stops at
/.As a result, a
denypattern with a non-trailing wildcard - e.g.shell:cmd=*rm -rf*- matchesrm -rf tmpbut notrm -rf /. The docstring onmatchGlobsays "we want*to match any charactersincluding spaces," but that only holds for the fast path; the
filepath.Matchfallback silently reintroducesseparator-boundary behavior. Because this is a
denyrule, the failure direction is fail-open.Expected Behavior
A
denypattern with a wildcard should match the same command regardless of whether the argument valuehappens to contain a
/.shell:cmd=*rm -rf*should deny bothrm -rf tmpandrm -rf /.Actual Behavior
shell:cmd=*rm -rf*deniesrm -rf tmpbut returnsAskforrm -rf /- the command is not blocked.The decision varies with argument content and with OS (separator is
/on Unix,\on Windows).Steps to Reproduce
Config-level: configure a deny rule with a non-trailing wildcard and invoke the shell tool with an
argument containing
/:shellwithcmd = "rm -rf tmp"→ denied ✅shellwithcmd = "rm -rf /"→ not denied (falls through to Ask) ❌Isolated unit reproduction against
pkg/permissions(the checker's public API + the matcher):go test ./pkg/permissions/ -run 'TestDenyGlob...|TestMatchGlob...' -vDocker Agent version
Docker version 29.5.3, build d1c06ef
OS & terminal
Reproduced on Linux (in a
golang:1.26.4container). The separator-boundary behavior is inherent tofilepath.Matchand also affects Windows (where the boundary is\), so the result is platform-dependent.Model used
N/A - this is in the permission matcher; it's independent of the model.
Error output
--- FAIL: TestDenyGlobDoesNotCrossPathSeparator (0.00s) Not equal: expected: 2 (Deny) actual: 0 (Ask) Messages: deny rule must also block 'rm -rf /' (the most dangerous case) --- FAIL: TestMatchGlobCrossesPathSeparator (0.00s) Should be true Messages: "*rm -rf*" should match "rm -rf /" FAIL github.com/docker/docker-agent/pkg/permissions (Decision enum: `Ask=0, Allow=1, Deny=2`.)Screenshots
No response
Additional context
Askstill prompts, so this is a degraded control rather thanimmediate silent execution - but it becomes a real bypass under
--yolo, non-interactive / auto-approvalruns, or when an overlapping
allowrule is present. The samefilepath.Matchpath also breaksallowrules, but those fail closed.
rm*,sudo*) hit the fast path and work, which tends to hide this in casual testing.