summaryrefslogtreecommitdiff
path: root/internal/tui/eventstream/export.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-13 10:27:15 +0300
committerPaul Buetow <paul@buetow.org>2026-05-13 10:27:15 +0300
commita21c653c9939ac82b181709dc745f017fb3b8a8a (patch)
tree9aac7254da11fddb66895bc7b141ba8618e5d69f /internal/tui/eventstream/export.go
parent62104fbcabf811b6cd31db15f0f72db1f9d3c6e6 (diff)
fix: prevent path traversal in TUI stream CSV export filename
User-supplied filenames are now sanitised through filepath.Base before being joined with exportDir, so inputs like "../../etc/passwd" can no longer write files outside the intended export directory. Pure directory references ("..") are rejected outright. Two new tests cover both the unit-level sanitisation and the end-to-end exportRowsToCSV path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/tui/eventstream/export.go')
-rw-r--r--internal/tui/eventstream/export.go21
1 files changed, 18 insertions, 3 deletions
diff --git a/internal/tui/eventstream/export.go b/internal/tui/eventstream/export.go
index 155a551..1aa4313 100644
--- a/internal/tui/eventstream/export.go
+++ b/internal/tui/eventstream/export.go
@@ -192,15 +192,30 @@ func exportRowsToCSV(rows []StreamEvent, exportDir, filename string) (string, er
return absPath, nil
}
+// ensureCSVFilename validates and normalises a user-supplied export filename.
+// It strips any directory components (preventing path traversal outside
+// exportDir) and rejects names that resolve to "." or "..". A ".csv"
+// extension is appended when the caller omits it.
func ensureCSVFilename(name string) (string, error) {
clean := strings.TrimSpace(name)
if clean == "" {
return "", errors.New("filename cannot be empty")
}
- if strings.HasSuffix(strings.ToLower(clean), ".csv") {
- return clean, nil
+
+ // Strip all directory components so that inputs such as
+ // "../../etc/passwd" or "/absolute/path.csv" cannot escape exportDir.
+ base := filepath.Base(clean)
+
+ // filepath.Base returns "." for empty/dot inputs and ".." for a raw ".."
+ // component — both are unusable as a plain filename.
+ if base == "." || base == ".." {
+ return "", errors.New("filename must not be a directory reference")
+ }
+
+ if strings.HasSuffix(strings.ToLower(base), ".csv") {
+ return base, nil
}
- return clean + ".csv", nil
+ return base + ".csv", nil
}
// ExportSnapshotToCSV exports a fresh filtered snapshot from the current source