Skip to content

Commit ddf77cc

Browse files
author
SqlRush
committed
Classify safe Bash sort reads
1 parent c2813e1 commit ddf77cc

4 files changed

Lines changed: 73 additions & 1 deletion

File tree

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ test/parity/ # golden tests against TS/official behavior
286286
- 本轮补充:Bash read-only 分类新增 `sed` 保守只读子集,允许 `-n`/`--quiet``-e`/`--expression` 的纯打印/删除/退出/行号脚本读取安全相对路径,同时拒绝 `-i``-f`、写文件/执行命令脚本和越界路径。
287287
- 本轮补充:Bash read-only 分类新增 `awk`/`gawk`/`mawk`/`nawk` 极小安全子集,允许 `{print}``{print $1, $2}` 这类纯输出脚本读取安全相对路径,并拒绝脚本文件、重定向、`system`/复杂表达式和越界路径。
288288
- 本轮补充:Bash 文件读取类 read-only 命令新增 `cut`/`uniq`,复用安全相对路径 guard,允许常见字段/字符裁剪和去重读取,同时拒绝绝对路径、父目录和变量/URI/provider-like 路径。
289+
- 本轮补充:Bash read-only 分类新增 `sort` 简单只读子集,允许无输出文件/无外部程序的常见排序标志读取安全相对路径,并拒绝 `-o`/`--output`、临时目录/压缩程序/复杂带值排序参数和越界路径。
289290
- 本轮补充:Bash `rg` read-only 分类现在拒绝 `--pre`/`--pre=...` 外部预处理命令,避免 ripgrep 调用任意预处理器时仍进入只读快路径。
290291
- 本轮补充:Bash `go list` read-only 分类从只看子命令收敛到参数级 allowlist,允许常见查询参数和 `-mod=readonly/vendor`,拒绝 `-mod=mod``-modfile``-overlay`、未知 flag、缺值 flag 以及非本地 package pattern。
291292
- 本轮补充:Bash `find` read-only 分类现在拒绝 `-delete``-fprint``-fprint0``-fprintf``-fls` 等删除/写文件 action,避免同一命令既被标成 destructive 又进入只读快路径。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ After the first/second batch hardening, the Go rewrite now includes initial `int
119119
Covered behavior:
120120

121121
- `Glob`/`Grep` initial pure-Go file search tools, including recursive `**` glob matching, Glob absolute-pattern base-dir extraction, Glob official pattern/path-only strict schema, Glob/Grep working-directory-relative result paths, Glob default no-ignore/hidden search plus `CLAUDE_CODE_GLOB_NO_IGNORE`/`CLAUDE_CODE_GLOB_HIDDEN` env switches, Grep official VCS metadata directory exclusion (`.git`/`.svn`/`.hg`/`.bzr`/`.jj`/`.sl`), Grep hierarchical `.gitignore`/`.ignore`/`.rgignore`, Glob oldest-first modified/path sorting, Glob truncated tool-result hinting, regex and fixed-string grep (`fixed_strings`/`-F`), Grep `regex`/`regexp`/`--regexp`/`-e` pattern aliases, multiline dotall grep, glob/type filtering, Grep glob whitespace/comma multi-pattern and brace alternation, Glob/Grep path existence validation plus Glob directory-only path validation, `output_mode`/`outputMode` `files_with_matches`/`content`/`count` output modes, Grep `count`/`--count`/`-c` count-mode aliases, Grep files_with_matches file-count summaries, Grep count-mode occurrence/file summaries, Grep `--max-columns 500` long matching/context line omission placeholders, `context`/`before_context`/`after_context` and `-C`/`-B`/`-A` content context lines with official precedence and non-content modes ignoring context, `line_numbers`/`lineNumbers`/`-n` line-number control, `max_count`/`maxCount`/`-m` per-file match limiting, `offset`/`head_limit` pagination and content-mode pagination tool-result hinting, default 250-entry Grep head limit, `head_limit=0` unlimited behavior, `ignore_case`/`case_insensitive`/`caseInsensitive`/`-i` case-insensitive search aliases, `case_sensitive`/`--case-sensitive`/`-s` and `smart_case`/`--smart-case`/`-S` search policy aliases, and quoted semantic string coercion for Grep numeric/boolean inputs.
122-
- `Bash` initial shell execution, including command/timeout/description validation, `/bin/sh -c` execution, stdout/stderr/exit-code/timeout structured results, dynamic read-only/concurrency-safe/destructive classification, Git diff/log/show/status/ls-files/grep/rev-parse/branch/tag/ls-remote safe-flag validation, Git remote/push/reflog/stash/worktree/merge-base/describe/cat-file/for-each-ref/rev-list/blame/shortlog/config-get argument-aware safety classification, conservative read-only `sed` and `awk` classification for safe print/delete/quit/line-number or field-print scripts over relative paths, safe relative-path `cut`/`uniq` reads, destructive `find -delete/-exec rm` and `xargs rm` classification, safe-wrapper/env prefix normalization for Bash read-only/destructive classification (`time`/`nohup`/`timeout`/`nice`/`stdbuf`/`env`), destructive command detection after temporary environment assignments, permission-rule matching through the existing tool permission adapter, background command start, same-session `BashOutput` reads, and `KillBash` cancellation.
122+
- `Bash` initial shell execution, including command/timeout/description validation, `/bin/sh -c` execution, stdout/stderr/exit-code/timeout structured results, dynamic read-only/concurrency-safe/destructive classification, Git diff/log/show/status/ls-files/grep/rev-parse/branch/tag/ls-remote safe-flag validation, Git remote/push/reflog/stash/worktree/merge-base/describe/cat-file/for-each-ref/rev-list/blame/shortlog/config-get argument-aware safety classification, conservative read-only `sed` and `awk` classification for safe print/delete/quit/line-number or field-print scripts over relative paths, safe relative-path `cut`/`uniq` reads, simple safe `sort` reads, destructive `find -delete/-exec rm` and `xargs rm` classification, safe-wrapper/env prefix normalization for Bash read-only/destructive classification (`time`/`nohup`/`timeout`/`nice`/`stdbuf`/`env`), destructive command detection after temporary environment assignments, permission-rule matching through the existing tool permission adapter, background command start, same-session `BashOutput` reads, and `KillBash` cancellation.
123123
- `TodoWrite` initial todo state, including full-list writes, status/priority validation, duplicate-id rejection, one `in_progress` guard, structured result payloads, tool metadata state storage, and session-scoped local persistence/restore.
124124
- `WebFetch` initial URL fetch behavior, including URL/timeout/max-byte validation, HTTP GET, HEAD preflight, metadata/raw `skipWebFetchPreflight` skip-preflight, binary preflight GET skipping from content type or attachment filename, text/binary detection, truncation, non-2xx error marking, structured result payloads, HTML-to-text rendering, prompt-focused excerpts, prompt phrase scoring/metadata, and `WebFetch(domain:...)` permission-rule adaptation.
125125
- `WebFetch` HTML-to-text rendering now preserves anchor `href` values as link context and emits visible image text from `img` `alt`/`title`/`aria-label` plus `src`, the first `img srcset` candidate, or a `<picture><source srcset>` candidate, allowing prompt-focused excerpts to match image descriptions while avoiding duplicate URL link text and `javascript:` hrefs.

internal/tools/bash/tools.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2133,6 +2133,8 @@ func readOnlyWords(words []string) bool {
21332133
switch cmd {
21342134
case "ls", "cat", "head", "tail", "wc", "grep", "egrep", "fgrep", "rg", "find", "stat", "file", "du", "df", "cut", "uniq":
21352135
return readOnlyPathCommand(words)
2136+
case "sort":
2137+
return readOnlySort(words[1:])
21362138
case "sed":
21372139
return readOnlySed(words[1:])
21382140
case "awk", "gawk", "mawk", "nawk":
@@ -2191,6 +2193,66 @@ func readOnlyPathCommand(words []string) bool {
21912193
return true
21922194
}
21932195

2196+
func readOnlySort(args []string) bool {
2197+
for i := 0; i < len(args); i++ {
2198+
arg := args[i]
2199+
if arg == "--" {
2200+
for _, path := range args[i+1:] {
2201+
if !safeRelativeShellPathArg(path) {
2202+
return false
2203+
}
2204+
}
2205+
return true
2206+
}
2207+
if strings.HasPrefix(arg, "--") {
2208+
if !safeSortLongFlag(arg) {
2209+
return false
2210+
}
2211+
continue
2212+
}
2213+
if strings.HasPrefix(arg, "-") && arg != "-" {
2214+
if !safeSortShortFlags(arg) {
2215+
return false
2216+
}
2217+
continue
2218+
}
2219+
if !safeRelativeShellPathArg(arg) {
2220+
return false
2221+
}
2222+
}
2223+
return true
2224+
}
2225+
2226+
func safeSortLongFlag(flag string) bool {
2227+
if strings.Contains(flag, "=") {
2228+
return false
2229+
}
2230+
switch flag {
2231+
case "--ignore-leading-blanks", "--dictionary-order", "--ignore-case",
2232+
"--general-numeric-sort", "--human-numeric-sort", "--month-sort",
2233+
"--numeric-sort", "--random-sort", "--reverse", "--version-sort",
2234+
"--unique", "--zero-terminated", "--check":
2235+
return true
2236+
default:
2237+
return false
2238+
}
2239+
}
2240+
2241+
func safeSortShortFlags(flags string) bool {
2242+
if flags == "" || flags[0] != '-' || flags == "-" {
2243+
return false
2244+
}
2245+
for _, r := range flags[1:] {
2246+
switch r {
2247+
case 'b', 'c', 'C', 'd', 'f', 'g', 'h', 'M', 'n', 'R', 'r', 'u', 'V', 'z':
2248+
continue
2249+
default:
2250+
return false
2251+
}
2252+
}
2253+
return true
2254+
}
2255+
21942256
func readOnlyAwk(args []string) bool {
21952257
seenScript := false
21962258
for i := 0; i < len(args); i++ {

internal/tools/bash/tools_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ func TestBashCommandClassification(t *testing.T) {
240240
"cut -c 1-20 README.md",
241241
"uniq -c sorted.txt",
242242
"uniq -f 1 sorted.txt",
243+
"sort README.md",
244+
"sort -u names.txt",
245+
"sort -nr numbers.txt",
246+
"sort --reverse --unique names.txt",
243247
"grep -f patterns.txt README.md",
244248
"grep -fpatterns.txt README.md",
245249
"grep --file=patterns.txt README.md",
@@ -338,6 +342,11 @@ func TestBashCommandClassification(t *testing.T) {
338342
"grep TODO /etc/passwd",
339343
"cut -d, -f1 /etc/passwd",
340344
"uniq ../secret.txt",
345+
"sort /etc/passwd",
346+
"sort -o out.txt README.md",
347+
"sort --output=out.txt README.md",
348+
"sort --compress-program=gzip README.md",
349+
"sort -k 1,1 README.md",
341350
"grep -f/etc/passwd README.md",
342351
"grep --file=/etc/passwd README.md",
343352
"grep -f ../patterns.txt README.md",

0 commit comments

Comments
 (0)