diff --git a/cmd/compare.go b/cmd/compare.go index 4f3e22f..ee24c20 100644 --- a/cmd/compare.go +++ b/cmd/compare.go @@ -64,6 +64,11 @@ Examples: RunE: func(cmd *cobra.Command, args []string) error { format, _ := cmd.Flags().GetString("format") savePath, _ := cmd.Flags().GetString("save") + if savePath != "" { + if err := validateCompareSavePath(savePath); err != nil { + return err + } + } format = strings.ToLower(strings.TrimSpace(format)) if format == "" { format = "terminal" @@ -179,6 +184,14 @@ func saveCompareOutput(path string, data []byte) error { return os.WriteFile(path, data, 0644) } +func validateCompareSavePath(path string) error { + cleanPath := filepath.Clean(path) + if filepath.IsAbs(path) || cleanPath == ".." || strings.HasPrefix(cleanPath, ".."+string(filepath.Separator)) { + return fmt.Errorf("save path must stay within the current directory: %q", path) + } + return nil +} + func init() { rootCmd.AddCommand(compareCmd) compareCmd.Flags().String("format", "terminal", "Output format: terminal, html, json, markdown") diff --git a/cmd/compare_test.go b/cmd/compare_test.go new file mode 100644 index 0000000..82d7a9b --- /dev/null +++ b/cmd/compare_test.go @@ -0,0 +1,26 @@ +package cmd + +import "testing" + +func TestValidateCompareSavePath(t *testing.T) { + tests := []struct { + name string + path string + valid bool + }{ + {name: "filename", path: "report.html", valid: true}, + {name: "nested relative path", path: "reports/compare.json", valid: true}, + {name: "parent traversal", path: "../report.html", valid: false}, + {name: "nested parent traversal", path: "reports/../../report.html", valid: false}, + {name: "absolute path", path: "/tmp/report.html", valid: false}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := validateCompareSavePath(test.path) == nil + if got != test.valid { + t.Fatalf("validateCompareSavePath(%q) valid = %v, want %v", test.path, got, test.valid) + } + }) + } +}