From 0d2f217f28a1403dee6499552ed5054f29ef768b Mon Sep 17 00:00:00 2001 From: Tw93 Date: Sat, 14 Mar 2026 08:26:45 +0800 Subject: [PATCH] security: add regression tests for validatePath with special chars - Add TestValidatePath covering Chinese, emoji, and special characters - Add TestValidatePathWithChineseAndSpecialChars for filesystem tests - Fix validatePath to detect .. components without rejecting valid paths Ensures paths with $, ;, :, emoji, Chinese chars are not rejected while still blocking path traversal attempts. --- cmd/analyze/delete.go | 11 +++--- cmd/analyze/delete_test.go | 81 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/cmd/analyze/delete.go b/cmd/analyze/delete.go index a26a4d9..3b5ff1e 100644 --- a/cmd/analyze/delete.go +++ b/cmd/analyze/delete.go @@ -153,7 +153,7 @@ func moveToTrash(path string) error { } // validatePath checks path safety for external commands. -// Returns error if path is empty, relative, contains null bytes, or escapes root. +// Returns error if path is empty, relative, contains null bytes, or has traversal. func validatePath(path string) error { if path == "" { return fmt.Errorf("path is empty") @@ -164,10 +164,11 @@ func validatePath(path string) error { if strings.Contains(path, "\x00") { return fmt.Errorf("path contains null bytes") } - // Ensure Clean doesn't radically alter the path (path traversal check). - clean := filepath.Clean(path) - if !strings.HasPrefix(clean, "/") { - return fmt.Errorf("path escapes root: %s", path) + // Check for path traversal attempts (.. components). + for _, component := range strings.Split(path, string(filepath.Separator)) { + if component == ".." { + return fmt.Errorf("path contains traversal components: %s", path) + } } return nil } diff --git a/cmd/analyze/delete_test.go b/cmd/analyze/delete_test.go index af95431..4132e2e 100644 --- a/cmd/analyze/delete_test.go +++ b/cmd/analyze/delete_test.go @@ -81,3 +81,84 @@ func TestMoveToTrashNonExistent(t *testing.T) { t.Fatal("expected error for non-existent path") } } + +func TestValidatePath(t *testing.T) { + tests := []struct { + name string + path string + wantErr bool + }{ + // 基本合法路径 + {"absolute path", "/Users/test/file.txt", false}, + {"path with spaces", "/Users/test/My Documents/file.txt", false}, + {"root", "/", false}, + + // 中文路径 + {"chinese path", "/Users/test/中文文件夹/文件.txt", false}, + {"chinese mixed", "/Users/test/Downloads/报告2024.pdf", false}, + + // Emoji 路径 + {"emoji path", "/Users/test/📁文件夹/📝笔记.txt", false}, + {"emoji only", "/Users/test/🎉/🎊.txt", false}, + + // 特殊字符路径 (之前被错误拒绝的) + {"dollar sign", "/Users/test/$HOME/workspace", false}, + {"semicolon", "/Users/test/project;v2", false}, + {"colon", "/Users/test/project:2024", false}, + {"ampersand", "/Users/test/R&D/project", false}, + {"at sign", "/Users/test/user@domain", false}, + {"hash", "/Users/test/project#123", false}, + {"percent", "/Users/test/100% complete", false}, + {"exclamation", "/Users/test/important!.txt", false}, + {"single quote", "/Users/test/user's files", false}, + {"equals", "/Users/test/key=value", false}, + {"plus", "/Users/test/file+v2", false}, + {"brackets", "/Users/test/[2024] report", false}, + {"parentheses", "/Users/test/project (copy)", false}, + {"comma", "/Users/test/file, backup", false}, + + // 非法路径 + {"empty", "", true}, + {"relative", "relative/path", true}, + {"relative dot", "./file.txt", true}, + {"null byte", "/Users/test\x00/file", true}, + {"path traversal", "/Users/test/../../../etc", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePath(tt.path) + if (err != nil) != tt.wantErr { + t.Errorf("validatePath(%q) error = %v, wantErr %v", tt.path, err, tt.wantErr) + } + }) + } +} + +func TestValidatePathWithChineseAndSpecialChars(t *testing.T) { + // 专门测试之前会导致兼容性回退的路径 + parent := t.TempDir() + testCases := []struct { + name string + path string + }{ + {"chinese", "中文文件夹"}, + {"emoji", "📁 文档"}, + {"mixed", "报告-2024_v2 (终稿) [已审核]"}, + {"special", "Project$2024; Q1: R&D"}, + {"complex", "用户@公司 100% 完成!"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fullPath := filepath.Join(parent, tc.path) + if err := os.MkdirAll(fullPath, 0o755); err != nil { + t.Fatalf("mkdir %q: %v", tc.path, err) + } + + if err := validatePath(fullPath); err != nil { + t.Errorf("validatePath rejected valid path %q: %v", tc.path, err) + } + }) + } +}