// sed_security_test.go -- sed 命令安全验证的单元测试. // // 覆盖场景: // - e/E 标志检测(各种变体) // - w/W 标志检测 // - 安全的打印命令 // - 安全的替换命令 // - Unicode 同形攻击 // - 花括号拒绝 // - 反斜杠分隔符拒绝 // - 地址解析 package permission import ( "testing" ) // TestCheckSedCommand_DangerousEFlag 测试 e 标志(执行 shell 命令)检测 func TestCheckSedCommand_DangerousEFlag(t *testing.T) { tests := []struct { name string cmd string }{ {"s 命令 e 标志", "sed 's/old/new/e' file"}, {"s 命令 ge 标志", "sed 's/old/new/ge' file"}, {"s 命令 E 标志", "sed 's/old/new/E' file"}, {"独立 e 命令", "sed 'e ls' file"}, {"带地址的 e 命令", "sed '1e ls' file"}, {"-e 选项中的 e 标志", "sed -e 's/old/new/e' file"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := CheckSedCommand(tt.cmd, false) if result.Safe { t.Errorf("CheckSedCommand(%q) 应为不安全, 但返回 Safe=true", tt.cmd) } if result.Pattern != "dangerous" && result.Pattern != "unknown" { // e 标志应被识别为 dangerous t.Logf("Pattern=%q, Reason=%q", result.Pattern, result.Reason) } }) } } // TestCheckSedCommand_DangerousWFlag 测试 w 标志(写入文件)检测 func TestCheckSedCommand_DangerousWFlag(t *testing.T) { tests := []struct { name string cmd string }{ {"s 命令 w 标志", "sed 's/old/new/w output.txt' file"}, {"s 命令 gw 标志", "sed 's/old/new/gw output.txt' file"}, {"独立 w 命令", "sed 'w output.txt' file"}, {"独立 W 命令", "sed 'W output.txt' file"}, {"带地址的 w 命令", "sed '1,5w output.txt' file"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := CheckSedCommand(tt.cmd, false) if result.Safe { t.Errorf("CheckSedCommand(%q) 应为不安全, 但返回 Safe=true", tt.cmd) } }) } } // TestCheckSedCommand_SafePrint 测试安全的打印命令 func TestCheckSedCommand_SafePrint(t *testing.T) { tests := []struct { name string cmd string }{ {"打印第一行", "sed -n '1p' file"}, {"打印范围", "sed -n '1,5p' file"}, {"打印到末尾", "sed -n '5,$p' file"}, {"打印末行", "sed -n '$p' file"}, {"-ne 融合", "sed -ne '1,5p' file"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := CheckSedCommand(tt.cmd, false) if !result.Safe { t.Errorf("CheckSedCommand(%q) 应为安全, 但返回 Safe=false, Reason=%q", tt.cmd, result.Reason) } if result.Pattern != "print" { t.Errorf("Pattern 应为 'print', 实际 %q", result.Pattern) } }) } } // TestCheckSedCommand_SafeSubstitution 测试安全的替换命令 func TestCheckSedCommand_SafeSubstitution(t *testing.T) { tests := []struct { name string cmd string }{ {"简单替换", "sed 's/old/new/' file"}, {"全局替换", "sed 's/old/new/g' file"}, {"不区分大小写", "sed 's/old/new/gi' file"}, {"带打印标志", "sed 's/old/new/gp' file"}, {"-e 选项", "sed -e 's/old/new/g' file"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := CheckSedCommand(tt.cmd, false) if !result.Safe { t.Errorf("CheckSedCommand(%q) 应为安全, 但返回 Safe=false, Reason=%q", tt.cmd, result.Reason) } if result.Pattern != "substitution" { t.Errorf("Pattern 应为 'substitution', 实际 %q", result.Pattern) } }) } } // TestCheckSedCommand_UnicodeHomograph 测试非 ASCII 字符拒绝 func TestCheckSedCommand_UnicodeHomograph(t *testing.T) { // 用 Cyrillic 'е' (U+0435) 替代 Latin 'e' cmd := "sed 's/old/n\u0435w/g' file" result := CheckSedCommand(cmd, false) if result.Safe { t.Errorf("含 Unicode 同形字符的 sed 命令应被拒绝") } if result.Pattern != "dangerous" { t.Errorf("Pattern 应为 'dangerous', 实际 %q", result.Pattern) } } // TestCheckSedCommand_CurlyBraces 测试花括号拒绝 func TestCheckSedCommand_CurlyBraces(t *testing.T) { tests := []string{ "sed '/pattern/{s/old/new/;p}' file", "sed '1,5{d}' file", } for _, cmd := range tests { result := CheckSedCommand(cmd, false) if result.Safe { t.Errorf("花括号 sed 命令应被拒绝: %q", cmd) } } } // TestCheckSedCommand_BackslashDelimiter 测试反斜杠分隔符拒绝 func TestCheckSedCommand_BackslashDelimiter(t *testing.T) { cmd := `sed 's\old\new\' file` result := CheckSedCommand(cmd, false) if result.Safe { t.Errorf("反斜杠分隔符的 sed 命令应被拒绝") } } // TestCheckSedCommand_NonSlashDelimiter 测试非 / 分隔符拒绝 func TestCheckSedCommand_NonSlashDelimiter(t *testing.T) { tests := []string{ "sed 's|old|new|g' file", "sed 's#old#new#g' file", "sed 's@old@new@g' file", } for _, cmd := range tests { result := CheckSedCommand(cmd, false) if result.Safe { t.Errorf("非 / 分隔符的 sed 命令应被拒绝: %q", cmd) } } } // TestCheckSedCommand_EmptyCommand 测试空命令 func TestCheckSedCommand_EmptyCommand(t *testing.T) { result := CheckSedCommand("", false) if result.Safe { t.Error("空命令应为不安全") } } // TestCheckSedCommand_NoSedFound 测试无 sed 命令名 func TestCheckSedCommand_NoSedFound(t *testing.T) { result := CheckSedCommand("grep pattern file", false) if result.Safe { t.Error("非 sed 命令应为不安全") } } // TestExtractSedExpressions 测试表达式提取 func TestExtractSedExpressions(t *testing.T) { tests := []struct { name string args []string wantExpr []string wantN bool }{ { "单个内联表达式", []string{"s/old/new/g", "file"}, []string{"s/old/new/g"}, false, }, { "-e 选项", []string{"-e", "s/old/new/g", "file"}, []string{"s/old/new/g"}, false, }, { "多个 -e", []string{"-e", "s/a/b/", "-e", "s/c/d/", "file"}, []string{"s/a/b/", "s/c/d/"}, false, }, { "-n 标志", []string{"-n", "1,5p", "file"}, []string{"1,5p"}, true, }, { "-ne 融合", []string{"-ne", "1,5p", "file"}, []string{"1,5p"}, true, }, { "--expression=", []string{"--expression=s/old/new/g", "file"}, []string{"s/old/new/g"}, false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { exprs, hasN, errMsg := extractSedExpressions(tt.args) if errMsg != "" { t.Fatalf("unexpected error: %s", errMsg) } if hasN != tt.wantN { t.Errorf("hasN=%v, want %v", hasN, tt.wantN) } if len(exprs) != len(tt.wantExpr) { t.Fatalf("expressions count: %d, want %d, got %v", len(exprs), len(tt.wantExpr), exprs) } for i, e := range exprs { if e != tt.wantExpr[i] { t.Errorf("expr[%d]=%q, want %q", i, e, tt.wantExpr[i]) } } }) } } // TestContainsDangerousOperations 测试危险操作检测 func TestContainsDangerousOperations(t *testing.T) { tests := []struct { name string expr string wantUnsafe bool }{ {"安全替换", "s/old/new/g", false}, {"e 标志", "s/old/new/e", true}, {"E 标志", "s/old/new/E", true}, {"w 标志", "s/old/new/w", true}, {"花括号", "{s/old/new/}", true}, {"独立 e", "e ls", true}, {"独立 w", "w output.txt", true}, {"安全打印", "1,5p", false}, {"安全删除", "1,5d", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reason := containsDangerousOperations(tt.expr, false) gotUnsafe := reason != "" if gotUnsafe != tt.wantUnsafe { t.Errorf("containsDangerousOperations(%q) unsafe=%v, want %v (reason=%q)", tt.expr, gotUnsafe, tt.wantUnsafe, reason) } }) } } // TestStripSedAddress 测试地址剥离 func TestStripSedAddress(t *testing.T) { tests := []struct { name string expr string wantCmd string }{ {"无地址", "s/a/b/", "s/a/b/"}, {"数字地址", "5s/a/b/", "s/a/b/"}, {"范围地址", "1,5s/a/b/", "s/a/b/"}, {"末行地址", "$s/a/b/", "s/a/b/"}, {"正则地址", "/pattern/s/a/b/", "s/a/b/"}, {"纯 p", "p", "p"}, {"数字 p", "1p", "p"}, {"范围 p", "1,5p", "p"}, {"空", "", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := stripSedAddress(tt.expr) if got != tt.wantCmd { t.Errorf("stripSedAddress(%q) = %q, want %q", tt.expr, got, tt.wantCmd) } }) } } // TestIsLinePrintingCommand 测试行打印命令验证 func TestIsLinePrintingCommand(t *testing.T) { tests := []struct { name string expr string want bool }{ {"裸 p", "p", true}, {"1p", "1p", true}, {"1,5p", "1,5p", true}, {"$p", "$p", true}, {"5,$p", "5,$p", true}, {"不是 p", "1,5d", false}, {"s 命令", "s/a/b/", false}, {"空", "", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := isLinePrintingCommand(tt.expr) if got != tt.want { t.Errorf("isLinePrintingCommand(%q) = %v, want %v", tt.expr, got, tt.want) } }) } } // TestCheckSedCommand_AllowFileWrites 测试 allowFileWrites 选项 func TestCheckSedCommand_AllowFileWrites(t *testing.T) { // w 标志在 allowFileWrites=true 时应该被允许 cmd := "sed 's/old/new/w output.txt' file" result := CheckSedCommand(cmd, false) if result.Safe { t.Error("w 标志在 allowFileWrites=false 时应被拒绝") } // 注意:CheckSedCommand 内部传 allowFileWrites 给 containsDangerousOperations // 这里我们直接测试 containsDangerousOperations reason := containsDangerousOperations("s/old/new/w", true) if reason != "" { t.Errorf("w 标志在 allowFileWrites=true 时不应被拒绝, reason=%q", reason) } }