From 3c02021080318d1d04c2bf3c8417ecf65ce842be Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 02:40:22 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20predictable=20temp=20file=20symlink=20vulnerabilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced uses of `os.WriteFile` with a predictable temporary filename `path + ".tmp"` in `os.TempDir()` with `os.CreateTemp` to generate unpredictable temporary files for atomic writes. This mitigates a classic symlink attack where an attacker could overwrite arbitrary files with the privileges of the user running the program. Co-authored-by: himattm <6266621+himattm@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ internal/burnrate/burnrate.go | 14 ++++++++++++-- internal/sparkline/sparkline.go | 16 +++++++++++++--- 3 files changed, 29 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..dc9dec6 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-05-24 - Predictable Temp File Creation Symlink Attack +**Vulnerability:** Found uses of predictable temporary file names in `os.TempDir()` (e.g., `tmpPath := path + ".tmp"`) followed by `os.WriteFile` in `internal/burnrate/burnrate.go` and `internal/sparkline/sparkline.go`. This allows for local symlink attacks where an attacker can overwrite arbitrary files by creating a symlink at the predictable path before the application writes to it. +**Learning:** These existed because predictable `.tmp` extensions were concatenated onto predictable file paths for intermediate atomic writes instead of generating cryptographically random filenames. +**Prevention:** Use `os.CreateTemp` to safely create unpredictable temporary files. Always use `filepath.Dir(path)` as the first argument to ensure the temporary file is created on the same filesystem/device as the target file, which avoids cross-device rename errors. Also, be sure to `Chmod` appropriately and `Close` the file descriptor before calling `os.Rename`. diff --git a/internal/burnrate/burnrate.go b/internal/burnrate/burnrate.go index 8cb680c..e92c226 100644 --- a/internal/burnrate/burnrate.go +++ b/internal/burnrate/burnrate.go @@ -64,10 +64,20 @@ 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 { + f, err := os.CreateTemp(filepath.Dir(path), "prism-burn-*.tmp") + if err != nil { + return nil, false, err + } + tmpPath := f.Name() + + if _, err := f.Write(data); err != nil { + f.Close() + os.Remove(tmpPath) return nil, false, err } + f.Chmod(0644) + f.Close() + 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..b847f38 100644 --- a/internal/sparkline/sparkline.go +++ b/internal/sparkline/sparkline.go @@ -138,11 +138,21 @@ func Save(sessionID, metric string, b *Buffer) { if err != nil { return } - tmp := path + ".tmp" - if err := os.WriteFile(tmp, data, 0644); err != nil { + f, err := os.CreateTemp(filepath.Dir(path), "prism-spark-*.tmp") + if err != nil { + return + } + tmp := f.Name() + if _, err := f.Write(data); err != nil { + f.Close() + os.Remove(tmp) return } - os.Rename(tmp, path) + f.Chmod(0644) + f.Close() + if err := os.Rename(tmp, path); err != nil { + os.Remove(tmp) + } } // PushAndSave is a convenience that loads, pushes, saves, and returns the buffer