diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-13 10:27:15 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-13 10:27:15 +0300 |
| commit | a21c653c9939ac82b181709dc745f017fb3b8a8a (patch) | |
| tree | 9aac7254da11fddb66895bc7b141ba8618e5d69f /internal/tui/eventstream/export.go | |
| parent | 62104fbcabf811b6cd31db15f0f72db1f9d3c6e6 (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.go | 21 |
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 |
