From 82291d86d8be39265df058e2a081bc4fd8aa8068 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 02:20:47 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Fix=20symli?= =?UTF-8?q?nk=20vulnerability=20in=20atomic=20writes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed a critical security vulnerability where predictable temporary file names (.tmp) were used for atomic file operations. By switching to os.CreateTemp with a random suffix, this prevents a malicious actor from orchestrating a symlink attack (CWE-377) during writes. Furthermore, we explicitly close the file before os.Rename to ensure cross-platform compatibility (Windows). Co-authored-by: himattm <6266621+himattm@users.noreply.github.com> --- .jules/sentinel.md | 7 +++++++ internal/burnrate/burnrate.go | 22 ++++++++++++++++++++-- internal/sparkline/sparkline.go | 25 +++++++++++++++++++++++-- 3 files changed, 50 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..afee6c8 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,7 @@ +## 2025-02-18 - Fix Predictable Temporary File Symlink Vulnerability + +**Vulnerability:** The code previously used string concatenation (e.g., `path + ".tmp"`) with `os.WriteFile` and `os.Rename` for atomic file writes in shared directories like `os.TempDir()`. This pattern is vulnerable to symlink attacks, where an attacker could pre-create a symlink with the predictable name pointing to an arbitrary file they want to overwrite. + +**Learning:** Predictable temporary file names in shared directories (like `/tmp`) are inherently insecure for atomic writes. Even if the content being written isn't malicious, the *location* being written to can be controlled via symlinks, leading to arbitrary file overwrite or corruption issues. Furthermore, creating a file with `os.CreateTemp` ensures the OS opens it with `O_EXCL`, preventing any prior symlink from being followed. Finally, on Windows, file handles must be explicitly closed before the `os.Rename` step, or it will fail. + +**Prevention:** For atomic writes in shared directories, *always* use `os.CreateTemp` with an unpredictable pattern (like `prism-*.tmp`). Ensure the temporary file is created in the same directory as the target file (using `filepath.Dir()`) to avoid cross-device rename errors. Explicitly `Chmod` to match required permissions, handle the error gracefully by closing and removing the temporary file, and explicitly close the file descriptor before calling `os.Rename`. diff --git a/internal/burnrate/burnrate.go b/internal/burnrate/burnrate.go index 8cb680c..c4455d1 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 { + // Use os.CreateTemp to avoid symlink vulnerability with predictable names + tmpFile, err := os.CreateTemp(filepath.Dir(path), "prism-burn-*.tmp") + 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 + } + // explicit close is important especially on Windows before rename + if err := tmpFile.Close(); err != nil { + os.Remove(tmpPath) + return nil, false, err + } + if err := os.Chmod(tmpPath, 0644); 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..d66b692 100644 --- a/internal/sparkline/sparkline.go +++ b/internal/sparkline/sparkline.go @@ -138,10 +138,31 @@ func Save(sessionID, metric string, b *Buffer) { if err != nil { return } - tmp := path + ".tmp" - if err := os.WriteFile(tmp, data, 0644); err != nil { + + // Use os.CreateTemp to avoid symlink vulnerability with predictable names + tmpFile, err := os.CreateTemp(filepath.Dir(path), "prism-spark-*.tmp") + if err != nil { + return + } + tmp := tmpFile.Name() + + if _, err := tmpFile.Write(data); err != nil { + tmpFile.Close() + os.Remove(tmp) + return + } + + // explicit close is important especially on Windows before rename + if err := tmpFile.Close(); err != nil { + os.Remove(tmp) + return + } + + if err := os.Chmod(tmp, 0644); err != nil { + os.Remove(tmp) return } + os.Rename(tmp, path) }