From 9cc20ed20e7114ee7e68ec845de835294b399742 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:02:52 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Fix=20predi?= =?UTF-8?q?ctable=20temporary=20filename=20vulnerabilities=20in=20atomic?= =?UTF-8?q?=20writes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced predictable temp file creation (`path + ".tmp"`) with `os.CreateTemp` for secure atomic file writes in `internal/burnrate/burnrate.go` and `internal/sparkline/sparkline.go` to prevent symlink attacks. Co-authored-by: himattm <6266621+himattm@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ internal/burnrate/burnrate.go | 22 ++++++++++++++++++++-- internal/sparkline/sparkline.go | 27 ++++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..6c54982 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-05-18 - Prevent Predictable Temporary Filenames during Atomic File Writes +**Vulnerability:** Found predictable temporary filenames in atomic file writes (e.g., `path + ".tmp"`) using `os.WriteFile` in `internal/burnrate/burnrate.go` and `internal/sparkline/sparkline.go`. This opens up symlink attacks during the predictable write, leading to potential unintended file overrides and privilege escalation. +**Learning:** It existed because `os.WriteFile` + `os.Rename` is a common but incomplete pattern for atomic file writes. The initial write must go to a file with an unpredictable name to prevent an attacker from creating a symlink before the write happens. +**Prevention:** Always use `os.CreateTemp` to generate an unpredictable temporary file for the initial write. Ensure it is created in the same directory as the target path (`filepath.Dir(path)`), close the file handle, and perform the `os.Rename` with explicit permission handling (`Chmod`). diff --git a/internal/burnrate/burnrate.go b/internal/burnrate/burnrate.go index 8cb680c..90286ca 100644 --- a/internal/burnrate/burnrate.go +++ b/internal/burnrate/burnrate.go @@ -64,10 +64,28 @@ func LoadOrCreateSnapshotAt(sessionID string, currentCost float64, now time.Time return nil, false, err } - tmpPath := path + ".tmp" - if err := os.WriteFile(tmpPath, data, 0644); err != nil { + dir := filepath.Dir(path) + tmpFile, err := os.CreateTemp(dir, "prism-burn-*") + if err != nil { + return nil, false, err + } + tmpPath := tmpFile.Name() + + if _, err := tmpFile.Write(data); err != nil { + tmpFile.Close() + os.Remove(tmpPath) + return nil, false, err + } + if err := tmpFile.Chmod(0644); err != nil { + tmpFile.Close() + os.Remove(tmpPath) + return nil, false, err + } + if err := tmpFile.Close(); err != nil { + os.Remove(tmpPath) return nil, false, err } + if err := os.Rename(tmpPath, path); err != nil { os.Remove(tmpPath) return nil, false, err diff --git a/internal/sparkline/sparkline.go b/internal/sparkline/sparkline.go index f4c755c..ecca58a 100644 --- a/internal/sparkline/sparkline.go +++ b/internal/sparkline/sparkline.go @@ -138,11 +138,32 @@ func Save(sessionID, metric string, b *Buffer) { if err != nil { return } - tmp := path + ".tmp" - if err := os.WriteFile(tmp, data, 0644); err != nil { + + dir := filepath.Dir(path) + tmpFile, err := os.CreateTemp(dir, "prism-spark-*") + if err != nil { + return + } + tmpPath := tmpFile.Name() + + if _, err := tmpFile.Write(data); err != nil { + tmpFile.Close() + os.Remove(tmpPath) return } - os.Rename(tmp, path) + if err := tmpFile.Chmod(0644); err != nil { + tmpFile.Close() + os.Remove(tmpPath) + return + } + if err := tmpFile.Close(); err != nil { + os.Remove(tmpPath) + return + } + + if err := os.Rename(tmpPath, path); err != nil { + os.Remove(tmpPath) + } } // PushAndSave is a convenience that loads, pushes, saves, and returns the buffer