Skip to content

Commit efaa1a8

Browse files
author
SqlRush
committed
Constrain Bash date read-only flags
1 parent 1bdfcca commit efaa1a8

4 files changed

Lines changed: 102 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
@@ -256,6 +256,7 @@ test/parity/ # golden tests against TS/official behavior
256256
- 本轮补充:Bash `grep`/`rg` read-only 分类现在会把 pattern-file 参数 `-f FILE``-fFILE``--file=FILE` 当作路径读取处理,缺值、绝对路径和 `..` 路径不再进入 read-only fast path。
257257
- 本轮补充:Bash `wc`/`du``--files0-from``find -files0-from` 不再进入 read-only fast path,避免通过受检列表文件间接读取未校验的绝对路径、父目录路径或敏感路径。
258258
- 本轮补充:Bash `file -f`/`--files-from` 这类间接路径列表读取,以及 `file -C`/`--compile` magic 编译形态不再进入 read-only fast path;普通 `file README.md``file --mime-type README.md` 保持只读。
259+
- 本轮补充:Bash `date` read-only 分类从无条件放行收敛到显示/解析/安全相对文件引用子集,拒绝 `date -s`/`--set`、legacy positional 设置时间和越界 `--reference`/`--file` 读取。
259260
- 本轮补充:Bash/PowerShell read-only 分类会先校验 tokenizer 视角的语法完整性,未闭合 quote 或末尾 escape/line-continuation 不再进入只读 fast path。
260261
- 本轮补充:Bash/PowerShell tokenizer 现在按 single-quoted literal 处理单引号内的 escape 字符,Bash 的 `\` 和 PowerShell 的 backtick 在单引号内不再导致错误的 quote/segment/token 状态。
261262
- 本轮补充:Bash/PowerShell 分类现在把未引用 newline 作为命令分隔符,并支持未引用 `#` 行注释剥离;注释文本不再污染分类,下一行命令仍保留 read-only/destructive 判断。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ Covered behavior:
214214
- `Bash` `find` read-only classification now rejects deleting or file-writing actions such as `-delete`, `-fprint`, `-fprint0`, `-fprintf`, and `-fls`, avoiding commands that mutate files being auto-allowed as read-only.
215215
- `Bash` safety classification now excludes `find -exec*` forms from read-only auto-allow and detects destructive commands passed through `find -exec*`/`xargs`, including shell `-c` scripts and safe-wrapper/env/assignment forms such as `env rm`, `timeout rm`, and `xargs -I{} env sh -c ...`.
216216
- `Bash` `file` read-only classification now rejects indirect `-f`/`--files-from` path-list reads and `-C`/`--compile` magic compilation forms while preserving ordinary relative-path file inspection.
217+
- `Bash` `date` read-only classification now allows only display, date-string parsing, and safe relative `--reference`/`--file` reads while rejecting `-s`/`--set`, legacy positional system-time setting, and absolute/parent date-file references.
217218
- `PowerShell` native/external file-reading/search read-only classification now applies relative-path guards to `where.exe /R`, `file`, `tree`, and `findstr` path positionals and path-valued flags, rejecting Windows-drive, UNC, URI/provider-like, parent-directory, and missing path-flag values before read-only auto-allow; `where.exe` no longer uses allow-all flag acceptance.
218219
- `PowerShell` native path guards now validate `findstr /D:` semicolon-separated directory lists item by item and treat `file -p` as a switch instead of a value-taking flag, closing path-guard bypasses through quoted directory lists or short-flag value misclassification.
219220
- `PowerShell` native `file` classification now rejects `-f`, attached `-ffile`, and `--files-from` indirect path-list reads, avoiding auto-allow for list-file contents that the classifier cannot inspect.

internal/tools/bash/tools.go

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2139,7 +2139,9 @@ func readOnlyWords(words []string) bool {
21392139
return readOnlySed(words[1:])
21402140
case "awk", "gawk", "mawk", "nawk":
21412141
return readOnlyAwk(words[1:])
2142-
case "pwd", "printf", "echo", "date", "whoami", "id", "uname", "printenv", "which", "type":
2142+
case "date":
2143+
return readOnlyDate(words[1:])
2144+
case "pwd", "printf", "echo", "whoami", "id", "uname", "printenv", "which", "type":
21432145
return true
21442146
case "env":
21452147
return readOnlyEnv(words)
@@ -2152,6 +2154,93 @@ func readOnlyWords(words []string) bool {
21522154
}
21532155
}
21542156

2157+
func readOnlyDate(args []string) bool {
2158+
for i := 0; i < len(args); i++ {
2159+
arg := args[i]
2160+
switch {
2161+
case arg == "--":
2162+
for _, rest := range args[i+1:] {
2163+
if !safeDateFormat(rest) {
2164+
return false
2165+
}
2166+
}
2167+
return true
2168+
case arg == "-u" || arg == "--utc" || arg == "--universal" || arg == "-R" || arg == "--rfc-email" || arg == "--debug":
2169+
continue
2170+
case arg == "-s" || arg == "--set" || strings.HasPrefix(arg, "--set="):
2171+
return false
2172+
case arg == "-d" || arg == "--date":
2173+
i++
2174+
if i >= len(args) || !safeDateValue(args[i]) {
2175+
return false
2176+
}
2177+
case strings.HasPrefix(arg, "-d") && len(arg) > 2:
2178+
if !safeDateValue(arg[2:]) {
2179+
return false
2180+
}
2181+
case strings.HasPrefix(arg, "--date="):
2182+
if !safeDateValue(strings.TrimPrefix(arg, "--date=")) {
2183+
return false
2184+
}
2185+
case arg == "-r" || arg == "--reference" || arg == "-f" || arg == "--file":
2186+
i++
2187+
if i >= len(args) || !safeRelativeShellPathArg(args[i]) {
2188+
return false
2189+
}
2190+
case strings.HasPrefix(arg, "--reference="):
2191+
if !safeRelativeShellPathArg(strings.TrimPrefix(arg, "--reference=")) {
2192+
return false
2193+
}
2194+
case strings.HasPrefix(arg, "--file="):
2195+
if !safeRelativeShellPathArg(strings.TrimPrefix(arg, "--file=")) {
2196+
return false
2197+
}
2198+
case strings.HasPrefix(arg, "-"):
2199+
if !safeDateDisplayFlag(arg) {
2200+
return false
2201+
}
2202+
default:
2203+
if !safeDateFormat(arg) {
2204+
return false
2205+
}
2206+
}
2207+
}
2208+
return true
2209+
}
2210+
2211+
func safeDateDisplayFlag(flag string) bool {
2212+
switch {
2213+
case flag == "-I":
2214+
return true
2215+
case strings.HasPrefix(flag, "-I"):
2216+
return safeDateValue(strings.TrimPrefix(flag, "-I"))
2217+
case strings.HasPrefix(flag, "--iso-8601"):
2218+
return safeOptionalDatePrecision(flag, "--iso-8601")
2219+
case strings.HasPrefix(flag, "--rfc-3339"):
2220+
return safeOptionalDatePrecision(flag, "--rfc-3339")
2221+
default:
2222+
return false
2223+
}
2224+
}
2225+
2226+
func safeOptionalDatePrecision(flag string, name string) bool {
2227+
if flag == name {
2228+
return true
2229+
}
2230+
value, ok := strings.CutPrefix(flag, name+"=")
2231+
return ok && safeDateValue(value)
2232+
}
2233+
2234+
func safeDateFormat(value string) bool {
2235+
value = strings.Trim(strings.TrimSpace(value), `"'`)
2236+
return strings.HasPrefix(value, "+") && safeDateValue(value[1:])
2237+
}
2238+
2239+
func safeDateValue(value string) bool {
2240+
value = strings.Trim(strings.TrimSpace(value), `"'`)
2241+
return value != "" && !strings.ContainsAny(value, "$`\x00><|;&\n\r")
2242+
}
2243+
21552244
func readOnlyPathCommand(words []string) bool {
21562245
command := filepathBase(words[0])
21572246
for i := 1; i < len(words); i++ {

internal/tools/bash/tools_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,11 @@ func TestBashCommandClassification(t *testing.T) {
266266
"pwd # rm -rf build",
267267
"printf '# rm -rf build'",
268268
"printf '$(rm -rf build)'",
269+
"date",
270+
"date -u +%Y-%m-%dT%H:%M:%SZ",
271+
"date --date 'next friday' +%F",
272+
"date --reference README.md +%s",
273+
"date --file dates.txt +%F",
269274
"pwd\nls -la",
270275
}
271276
for _, command := range readOnly {
@@ -400,6 +405,11 @@ func TestBashCommandClassification(t *testing.T) {
400405
"cat 'README.md",
401406
"git status --short \"",
402407
"printf hello \\",
408+
"date -s tomorrow",
409+
"date --set=tomorrow",
410+
"date 010112002026",
411+
"date --reference=/etc/passwd +%s",
412+
"date -f ../dates.txt",
403413
"cat 'README.md; rm -rf build",
404414
"pwd\nmake build",
405415
"pwd & ls -la",

0 commit comments

Comments
 (0)