Skip to content

#567 [security][tools] Bash の書き込み先の glob・brace を展開前の文字列で判定するため、中の symlink を通ってワークスペース外へ書き込める(#509 の調査で発見) - #573

Merged
Kewton merged 4 commits into
developfrom
feature/issue-567-security-tools-bash-glob-brace-symlink-509
Oct 1, 2026

Conversation

@Kewton

@Kewton Kewton commented Oct 1, 2026

Copy link
Copy Markdown
Owner

What changed

  • src/tools/path_guard.rs: ensure_bash_write_target now calls the new expansion check before its existing proof (a few lines).
  • src/tools/path_guard/bash_pattern.rs (new):
    • Expands braces (lists, nesting, numeric and character ranges, steps, zero padding). Both the zero-padded form and the zero-stripped form are checked.
    • Expands globs one component at a time, without following symlinks.
    • Runs the existing proof on the original string and on every expansion and match.
    • Rejects a match that resolves outside the root without listing below it.
    • Rejects .. reached through .* / .? / {.,}..
    • Caps brace expansion at 256 results and glob matches at 4096. Sizes are counted before expanding, and range arithmetic is overflow-checked.
    • Rejects shell syntax that globset cannot reproduce faithfully (POSIX bracket classes [:...:], [=, [.). Leftover literal {, }, , and \ are escaped before matching.
  • tests/issue567_bash_glob_write_targets.rs (new): integration tests through path_confinement_rejection.
  • No blanket rejection of glob characters. Reads such as ls src/*.rs and cat src/{main,lib}.rs, and quoted Next.js routes such as "src/app/[id]/page.tsx", stay allowed.

Verification

  • Runner verify: 15/15 pass (commandmate verify on the final head).
  • Before/after: on the pre-fix base, the new rejection tests (glob, .., brace, POSIX class, literal brace, zero-padded range) fail. With this change, they pass. The resource-cap tests hung before the fix and now return quickly.
  • Unchanged and still passing: tests/issue428_bash_path_tokens.rs and tests/bash_workspace_confinement.rs.
  • cargo mutants on bash_pattern.rs: all 172 mutants were measured, and unstable results were re-measured one at a time. The only survivors are equivalent boundary mutants in RangeSpec::bounded_count (the result stays min(count, limit + 1)). Timeouts are loop mutants that never finish, so they count as detected.
  • Merge-gate review (Claude_Sub): the first review found one blocker (POSIX classes and literal braces bypassed the match). It is fixed in this PR, and a fix-only recheck runs before merge.

Notes

Developed with the CommandMate parallel-dev harness (PM: Claude Code, leader: Claude_Dev, workers: Command Code).

🤖 Generated with Claude Code

Kewton and others added 4 commits October 1, 2026 10:45
Bash write targets that use glob (`* ? [`) or brace (`{a,b}`, `{1..9}`)
syntax were judged on the pre-expansion word. That name does not exist, so
the literal proof fell back to the nearest existing parent -- the workspace
root -- and allowed writes that the shell resolves onto an intermediate
symlink leaving the workspace, or onto a brace-produced `..`.

Expand the word the way the shell would -- braces first (nested, `,`,
ranges), then globs one component at a time without following symlinks --
and re-run the literal proof on every result. Fail closed once the brace
(256) or glob (4096) expansion limit is exceeded, and never read a directory
outside the workspace root.

- src/tools/path_guard.rs: split the literal proof into ensure_single_target
  and call the new child module from ensure_bash_write_target.
- src/tools/path_guard/bash_pattern.rs: new child module with the expansion.
- tests/issue567_bash_glob_write_targets.rs: reject/allow/cap/loop coverage.

判断: the brace cap 256 and glob match cap 4096 are the issue's suggested values; tests pin only that an overshoot is rejected, not the exact boundary.
読み替え: `tee */f` is allowed only in a normal workspace whose root holds no escaping symlink for `*` to match; the issue's "許可のまま" list is read that way.
本文に無い指摘: a glob used as a directory component must drop non-directory matches, otherwise `tee */f` is over-rejected whenever the root also holds a regular file.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…on up front

Follow-up to #567 (review H-02). Two blockers and three robustness gaps:

- POSIX bracket expressions (`[[:class:]]`, `[[=c=]]`, `[[.coll.]]`) are
  refused as unprovable: globset reads them as an ordinary class, so the shell
  would expand a word the guard silently passed through literally.
- A leftover `{`, `}`, `,`, or `\` is rewritten to a character-class literal
  (`[{]`, `[}]`, `[,]`, `[\\]`) before globset, because globset reads those as
  alternation/escaping while the shell reads them as ordinary characters.
  Without this, `{x}*` matched nothing and fell back to allowing the literal.
- Brace expansion is counted before expanding, so an over-limit word (256) is
  rejected without growing a queue; a range is sized before building a vector
  and its loop uses checked_add, so `{1..100000000}` and an i64-spanning range
  fail fast instead of allocating or overflowing.
- `{01..02}` is proven both zero-padded (`01`, bash 4+) and stripped (`1`),
  including when only one endpoint carries the leading zero.
- `dotglob` is documented with extglob/GLOBIGNORE/nocaseglob as a known limit.

判断: the requested cargo-mutants command (`--lib --test`, no filter) cannot complete inside the 30-minute cap on this machine: the lib suite is 2854 tests / ~45s / ~670s CPU each, so 172 mutants exceed the budget. Ran the integration selection to completion and cross-checked every survivor against the module's own unit tests.
読み替え: `{9223372036854775806..9223372036854775807}` is two safe inside values, so it is allowed, not rejected; the test pins no-panic + a time bound, and a genuinely overflowing span is asserted rejected.
本文に無い指摘: the asymmetric padding case `{01..2}`/`{1..02}` was not covered by the symmetric `{01..02}` test; added both.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Follow-up to #567 review L-02. The leader's mutant run left 12 survivors in the
new numeric-range code. Add direct unit coverage and an integration case:

- RangeSpec::values: full-list assertions for ascending/descending, step,
  endpoints, a single value, zero-padded (both forms) and character ranges.
- RangeSpec::bounded_count: counts for the same shapes, a span no step divides
  exactly, zero-padded doubling, a zero step, and the limit / limit+1 cap.
- Integration: `{k..m}`, `{1..3}` (with `2` -> outside) and `{1..5..2}` (with
  `3` -> outside) are rejected only because the range expands.

Manual per-mutant classification (apply mutation, run tests; not cargo-mutants):
10 of 12 killed. Two survive and are provably equivalent:
- bounded_count `limit as u128 + 1` -> `* 1`: `count > limit + 1` and
  `count > limit` agree at every count (<= limit -> count; == limit+1 and
  >= limit+2 -> limit+1), so no test can distinguish them.
- values `direction < 0` -> `<= 0`: direction is only ever +1 or -1, never 0,
  so the two conditions are identical.

判断: no production change needed. The `values -> vec![]` survivor did not reproduce: with it a range expands to nothing and the guard allows, which makes the reject-asserting tests fail, so the suite kills it (lib and integration).
本文に無い指摘: the reported 174:9 survivor is a stale-build artifact, not a test hole.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…e range step

Follow-up to #567 review (round 3).

- globset_pattern rewrote a literal `{`, `}`, `,`, or `\` into a class literal
  even inside an existing bracket, nesting it (`[,l]` -> `[[,]l]`). globset
  then read a different set, matched nothing, and fell back to the literal
  proof, so `[,l]inked-outside/` was allowed. It now returns None when the
  component contains `[` together with any of `{ } , \`, and the caller
  refuses the word as unprovable (same treatment as `[:`/`[=`/`[.`).
- range_spec used `step.abs()`, which panics in debug for i64::MIN
  (`{1..5..-9223372036854775808}`); it now uses `checked_abs()` and the word
  stays literal.

Tests: unit coverage for globset_pattern's None cases and the overflowing
step; an integration table rejecting `[,l]inked-outside/`,
`[{l]inked-outside/secret`, `[!,]inked-outside/secret`, `[l}]`, `[^{]`,
`[!{]*`, `[k-m,]`, plus rm/chmod forms; and allow cases confirming brackets
without `{ } , \` and braces without a bracket (quoted `[id]`,
`cat src/{main,lib}.rs`, `ls src/*.rs`, `tee src/*.rs`, `cp a.txt "src/[id]/x"`)
still pass.

判断: a bracket mixed with `{ } , \` is refused rather than expanding both spellings, because any rewrite nests the brackets and cannot be proven to match the shell.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
@Kewton
Kewton merged commit 87f7a77 into develop Oct 1, 2026
5 checks passed
@Kewton
Kewton deleted the feature/issue-567-security-tools-bash-glob-brace-symlink-509 branch October 1, 2026 11:48
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.

1 participant