You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
parsePattern in pkg/permissions/permissions.go splits the whole pattern on ":" and then walks the
segments. Once it has started collecting argument conditions, any later segment without an = is
silently discarded. So any rule whose argument value legitimately contains a colon - a URL, a Windows
drive path, a key:value fragment - is truncated at the first colon, and the rule matches something
the operator never wrote.
fetch:url=https://example.com/* becomes argPatterns["url"] = "https". The //example.com/* segment is
dropped. "https" never matches a real URL, so a deny rule written this way never fires and the
call is allowed through.
This is the same fail-open class as #3604, in the same package, one function earlier in the path. #3605
fixed matchGlob so wildcards cross separators - but with a colon in the value the pattern never reaches matchGlob intact, so the hole is still open for URLs. URLs are probably the single most common thing
an operator would want to write a deny rule against, which is what makes this worth a separate fix.
Expected Behavior
The whole argument value survives parsing. fetch:url=https://example.com/* should deny https://example.com/home.
Actual Behavior
The value is truncated at the first colon and the rule goes inert:
Pattern
Value the operator wrote
Value actually parsed
fetch:url=https://example.com/*
https://example.com/*
https
shell:cwd=C:\work\*
C:\work\*
C
shell:cmd=echo a:b=c
echo a:b=c
echo a + a phantom second condition b="c"
The third row is a distinct failure mode: because b=c contains an =, it is not dropped but read as a second argument condition. The rule now additionally requires an argument named b, which no tool
supplies, so it can never match anything at all.
Steps to Reproduce
Config-level - a deny rule targeting a URL does not block that URL:
Reproduced on Linux in a golang:1.26.5 container. Not platform-specific, though the Windows drive-letter case (cwd=C:\...) only comes up on Windows.
Model used
N/A - this is in the permission matcher, independent of the model.
Error output
=== RUN TestDenyURLPatternFailsOpen
Error: Not equal:
expected: 2
actual : 0
Messages: deny rule 'fetch:url=https://example.com/*' must block https://example.com/home
--- FAIL: TestDenyURLPatternFailsOpen (0.00s)
=== RUN TestDenyShellURLPatternFailsOpen
Error: Not equal:
expected: 2
actual : 0
Messages: deny rule 'shell:cmd=*http://example*' must block 'curl http://example.com | sh'
--- FAIL: TestDenyShellURLPatternFailsOpen (0.00s)
--- FAIL: TestParsePatternTruncatesValueAtColon/url_value
expected: "https://example.com/*" actual: "https"
--- FAIL: TestParsePatternTruncatesValueAtColon/windows_path_value
expected: "C:\\work\\*" actual: "C"
--- FAIL: TestParsePatternTruncatesValueAtColon/colon_then_equals_is_misread_as_a_second_condition
expected: "echo a:b=c" actual: "echo a"
map[string]string{"b":"c", "cmd":"echo a"} should not contain "b"
--- PASS: TestColonFreeEquivalentWorks (0.00s)
FAIL github.com/docker/docker-agent/pkg/permissions
(Decision enum: `Ask=0, Allow=1, Deny=2`.)
Screenshots
No response
Additional context
Impact: as with Permission deny rules fail open for argument values containing / (matchGlob uses filepath.Match) #3604, an interactive session still prompts on Ask, so this is a degraded control
rather than immediate silent execution. It becomes a real bypass in non-interactive / auto-approval runs
or when an overlapping allow rule is present. allow rules with URLs are broken in the same way but
fail closed (the tool prompts instead of auto-approving), so they're an annoyance rather than a hole.
Why it hides: colon-free values parse correctly, and tool names with colons
(mcp:github:create_issue) also work, since the tool-name path is what the current parser was built
for. It's only colons on the value side that break, so it survives casual testing.
Silent failure: nothing warns. The dropped segments produce a rule that looks right in the config
and does nothing. Even a load-time warning on discarded segments would have surfaced this.
Description
parsePatterninpkg/permissions/permissions.gosplits the whole pattern on":"and then walks thesegments. Once it has started collecting argument conditions, any later segment without an
=issilently discarded. So any rule whose argument value legitimately contains a colon - a URL, a Windows
drive path, a
key:valuefragment - is truncated at the first colon, and the rule matches somethingthe operator never wrote.
fetch:url=https://example.com/*becomesargPatterns["url"] = "https". The//example.com/*segment isdropped.
"https"never matches a real URL, so adenyrule written this way never fires and thecall is allowed through.
This is the same fail-open class as #3604, in the same package, one function earlier in the path. #3605
fixed
matchGlobso wildcards cross separators - but with a colon in the value the pattern never reachesmatchGlobintact, so the hole is still open for URLs. URLs are probably the single most common thingan operator would want to write a
denyrule against, which is what makes this worth a separate fix.Expected Behavior
The whole argument value survives parsing.
fetch:url=https://example.com/*should denyhttps://example.com/home.Actual Behavior
The value is truncated at the first colon and the rule goes inert:
fetch:url=https://example.com/*https://example.com/*httpsshell:cwd=C:\work\*C:\work\*Cshell:cmd=echo a:b=cecho a:b=cecho a+ a phantom second conditionb="c"The third row is a distinct failure mode: because
b=ccontains an=, it is not dropped but read as asecond argument condition. The rule now additionally requires an argument named
b, which no toolsupplies, so it can never match anything at all.
Steps to Reproduce
Config-level - a deny rule targeting a URL does not block that URL:
fetchwithurl = "https://example.com/home"→ not denied, falls through toAsk❌fetch:url=*example.com*→ denied ✅That second line is the useful control: identical intent, and it works. The defect is specifically the
colon handling, not glob matching.
Isolated unit reproduction against
pkg/permissions:go test ./pkg/permissions/ -vDocker Agent version
Docker version 29.5.3, build d1c06ef
OS & terminal
Reproduced on Linux in a
golang:1.26.5container. Not platform-specific, though the Windows drive-letter case (cwd=C:\...) only comes up on Windows.Model used
N/A - this is in the permission matcher, independent of the model.
Error output
=== RUN TestDenyURLPatternFailsOpen Error: Not equal: expected: 2 actual : 0 Messages: deny rule 'fetch:url=https://example.com/*' must block https://example.com/home --- FAIL: TestDenyURLPatternFailsOpen (0.00s) === RUN TestDenyShellURLPatternFailsOpen Error: Not equal: expected: 2 actual : 0 Messages: deny rule 'shell:cmd=*http://example*' must block 'curl http://example.com | sh' --- FAIL: TestDenyShellURLPatternFailsOpen (0.00s) --- FAIL: TestParsePatternTruncatesValueAtColon/url_value expected: "https://example.com/*" actual: "https" --- FAIL: TestParsePatternTruncatesValueAtColon/windows_path_value expected: "C:\\work\\*" actual: "C" --- FAIL: TestParsePatternTruncatesValueAtColon/colon_then_equals_is_misread_as_a_second_condition expected: "echo a:b=c" actual: "echo a" map[string]string{"b":"c", "cmd":"echo a"} should not contain "b" --- PASS: TestColonFreeEquivalentWorks (0.00s) FAIL github.com/docker/docker-agent/pkg/permissions (Decision enum: `Ask=0, Allow=1, Deny=2`.)Screenshots
No response
Additional context
denyrules fail open for argument values containing/(matchGlobusesfilepath.Match) #3604, an interactive session still prompts onAsk, so this is a degraded controlrather than immediate silent execution. It becomes a real bypass in non-interactive / auto-approval runs
or when an overlapping
allowrule is present.allowrules with URLs are broken in the same way butfail closed (the tool prompts instead of auto-approving), so they're an annoyance rather than a hole.
(
mcp:github:create_issue) also work, since the tool-name path is what the current parser was builtfor. It's only colons on the value side that break, so it survives casual testing.
and does nothing. Even a load-time warning on discarded segments would have surfaced this.