From ce65ea2e233b8c27aa32ef748d552c11cb8afa28 Mon Sep 17 00:00:00 2001 From: Mzack9999 Date: Thu, 29 Oct 2026 15:48:32 +0100 Subject: [PATCH] sanitize names --- pkg/logger/file/file.go | 7 ++++++- pkg/logger/file/file_test.go | 28 ++++++++++++++++++++++++++++ pkg/logger/logger.go | 16 +++++++++++++++- pkg/logger/logger_test.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 pkg/logger/file/file_test.go diff --git a/pkg/logger/file/file.go b/pkg/logger/file/file.go index c39aea7b..6b84d5c7 100644 --- a/pkg/logger/file/file.go +++ b/pkg/logger/file/file.go @@ -48,7 +48,12 @@ func (c *Client) Save(data types.HTTPTransaction) error { var err error logFile := fmt.Sprintf("%s.%s", data.Name, "txt") if c.options.OutputFolder != "" { - err = c.writeToFile(filepath.Join(c.options.OutputFolder, logFile), string(data.RawData)) + logPath := filepath.Join(c.options.OutputFolder, logFile) + // the name holds client data: never write outside the folder + if filepath.Dir(logPath) != filepath.Clean(c.options.OutputFolder) { + return fmt.Errorf("invalid log file name %q", logFile) + } + err = c.writeToFile(logPath, string(data.RawData)) } if c.options.OutputFile != "" && len(data.Data) > 0 { err = c.writeToFile(c.options.OutputFile, data.DataString) diff --git a/pkg/logger/file/file_test.go b/pkg/logger/file/file_test.go new file mode 100644 index 00000000..d57d7cd8 --- /dev/null +++ b/pkg/logger/file/file_test.go @@ -0,0 +1,28 @@ +package file + +import ( + "os" + "path/filepath" + "testing" + + "github.com/projectdiscovery/proxify/pkg/types" +) + +func TestSaveRejectsNameOutsideFolder(t *testing.T) { + root := t.TempDir() + logs := filepath.Join(root, "cwd", "proxify_logs") + client, err := New(&Options{OutputFolder: logs}) + if err != nil { + t.Fatal(err) + } + err = client.Save(types.HTTPTransaction{Name: "../../outside-1fde6388dae4f50d", RawData: []byte("data")}) + if err == nil { + t.Fatal("expected an error for a name that leaves the log folder") + } + if _, statErr := os.Stat(filepath.Join(root, "outside-1fde6388dae4f50d.txt")); !os.IsNotExist(statErr) { + t.Fatalf("file written outside the log folder: %v", statErr) + } + if err := client.Save(types.HTTPTransaction{Name: "example.com:8443-1fde6388dae4f50d", RawData: []byte("data")}); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index cc5c4a95..33663a21 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -420,7 +420,7 @@ func (l *Logger) storeWriter(outputdata types.HTTPTransaction) { return } - outputdata.Name = fmt.Sprintf("%s%s-%s", outputdata.Userdata.Host, outputdata.PartSuffix, outputdata.Userdata.ID) + outputdata.Name = fmt.Sprintf("%s%s-%s", safeFileName(outputdata.Userdata.Host), outputdata.PartSuffix, outputdata.Userdata.ID) if outputdata.Userdata.HasResponse && (!l.options.DumpRequest && !l.options.DumpResponse) { if outputdata.Userdata.Match != nil && *outputdata.Userdata.Match { outputdata.Name = outputdata.Name + ".match" @@ -451,6 +451,20 @@ func (l *Logger) storeWriter(outputdata types.HTTPTransaction) { } } +// safeFileName makes a Host header value safe to use in a file name. The +// value comes from the client, so every byte outside [A-Za-z0-9._:-] +// (path separators included) becomes '_'. +func safeFileName(host string) string { + return strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '_', r == ':', r == '-': + return r + default: + return '_' + } + }, host) +} + func isASCIICheckRequired(contentType string) bool { return stringsutil.ContainsAny(contentType, "application/octet-stream", "application/x-www-form-urlencoded") } diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go index 4b17131d..08ab5d1d 100644 --- a/pkg/logger/logger_test.go +++ b/pkg/logger/logger_test.go @@ -17,6 +17,7 @@ import ( "time" "github.com/projectdiscovery/martian/v3" + "github.com/projectdiscovery/proxify/pkg/logger/file" "github.com/projectdiscovery/proxify/pkg/logger/har" "github.com/projectdiscovery/proxify/pkg/types" ) @@ -457,3 +458,32 @@ func TestHARLoggingDoesNotReadLiveBody(t *testing.T) { t.Fatal("HAR logging consumed the live request body") } } + +func TestStoreWriterKeepsFilesInLogFolder(t *testing.T) { + root := t.TempDir() + logs := filepath.Join(root, "cwd", "proxify_logs") + if err := os.MkdirAll(filepath.Join(root, "outside"), 0o755); err != nil { + t.Fatal(err) + } + store, err := file.New(&file.Options{OutputFolder: logs}) + if err != nil { + t.Fatal(err) + } + l := &Logger{options: &OptionsLogger{OutputFolder: logs}, Store: []Store{store}} + for _, host := range []string{"../../outside/file", `..\..\outside\file`, "/etc/x", "a/../../b"} { + l.storeWriter(types.HTTPTransaction{Userdata: types.UserData{Host: host, ID: "1fde6388dae4f50d"}, RawData: []byte("GET / HTTP/1.1\r\n")}) + } + if entries, _ := os.ReadDir(filepath.Join(root, "outside")); len(entries) > 0 { + t.Fatalf("file written outside the log folder: %s", entries[0].Name()) + } + entries, err := os.ReadDir(logs) + if err != nil { + t.Fatal(err) + } + if len(entries) == 0 { + t.Fatalf("no log file in %s", logs) + } + if got, want := safeFileName("[::1]:8080"), "_::1_:8080"; got != want { + t.Fatalf("safeFileName = %q, want %q", got, want) + } +}