From e0343fb36fc9c463b6ce4e865f3d8412daa147aa Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 02:25:27 +0000 Subject: [PATCH] Fix predictable temporary filename symlink vulnerabilities Replaced predictable temporary file creation in internal/burnrate and internal/sparkline using `os.WriteFile` with unpredictable temporary filenames using `os.CreateTemp` to prevent symlink attacks. Set temporary files to original permission modes. Included documentation in `.jules/sentinel.md` for learning tracking. Co-authored-by: himattm <6266621+himattm@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ internal/burnrate/burnrate.go | 23 +++++++++++++++++++++-- internal/sparkline/sparkline.go | 24 ++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..c5299f1 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-02-14 - Predictable Temporary Filename Symlink Vulnerability +**Vulnerability:** Predictable temporary files were created using hardcoded extensions (e.g. `path + ".tmp"`) in shared directories like `/tmp`. This allows an attacker to pre-create a symlink at the predicted location, tricking the application into overwriting an arbitrary file. +**Learning:** Atomic file writes often involve creating a temporary file and renaming it. If the temporary filename is predictable and located in a shared directory, it is vulnerable to symlink attacks. +**Prevention:** Always use `os.CreateTemp` to generate unpredictable temporary filenames. Explicitly set permissions using `Chmod` if matching the original file permissions is necessary, and ensure `Close()` is called before `Rename()`. diff --git a/internal/burnrate/burnrate.go b/internal/burnrate/burnrate.go index 8cb680c..adcd534 100644 --- a/internal/burnrate/burnrate.go +++ b/internal/burnrate/burnrate.go @@ -64,10 +64,29 @@ 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 + } + + if err := f.Chmod(0644); err != nil { + f.Close() + os.Remove(tmpPath) return nil, false, err } + + if err := f.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..110d823 100644 --- a/internal/sparkline/sparkline.go +++ b/internal/sparkline/sparkline.go @@ -138,10 +138,30 @@ 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 + } + + if err := f.Chmod(0644); err != nil { + f.Close() + os.Remove(tmp) + return + } + + if err := f.Close(); err != nil { + os.Remove(tmp) return } + os.Rename(tmp, path) }