Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2026-05-04 - Fix Predictable Temporary File Names for Atomic Writes
**Vulnerability:** Predictable temporary file names used during atomic file writes (`path + ".tmp"`) are susceptible to symlink attacks, allowing an attacker to overwrite arbitrary files if they have write access to the temporary directory.
**Learning:** Atomic file writes often rely on temporary files, but using predictable names allows malicious actors to pre-create symlinks pointing to sensitive files.
**Prevention:** Always use `os.CreateTemp` to generate unpredictable temporary file names, create them in the same directory as the target file to avoid cross-device rename issues, and securely set permissions before writing.
23 changes: 21 additions & 2 deletions internal/burnrate/burnrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
tmpFile, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)+".*.tmp")
if err != nil {
return nil, false, err
}
tmpPath := tmpFile.Name()

if err := tmpFile.Chmod(0644); err != nil {
tmpFile.Close()
os.Remove(tmpPath)
return nil, false, err
}
Comment on lines +73 to +77
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The temporary file is created in the system's temporary directory (/tmp). Using 0644 permissions allows any local user to read the snapshot data, which contains session-specific information. Since os.CreateTemp already creates the file with secure 0600 permissions (owner read/write only), it is recommended to remove this Chmod call unless world-readability is explicitly required for other processes.


if _, err := tmpFile.Write(data); err != nil {
tmpFile.Close()
os.Remove(tmpPath)
return nil, false, err
}
Comment on lines +79 to 83
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure the atomic write is durable, tmpFile.Sync() should be called before closing the file. This guarantees that the data is flushed to stable storage before the rename operation occurs, preventing data loss or corruption in the event of a system crash.

Suggested change
if _, err := tmpFile.Write(data); err != nil {
tmpFile.Close()
os.Remove(tmpPath)
return nil, false, err
}
if _, err := tmpFile.Write(data); err != nil {
tmpFile.Close()
os.Remove(tmpPath)
return nil, false, err
}
if err := tmpFile.Sync(); 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
Expand Down
28 changes: 25 additions & 3 deletions internal/sparkline/sparkline.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,33 @@ func Save(sessionID, metric string, b *Buffer) {
if err != nil {
return
}
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, 0644); err != nil {
tmpFile, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)+".*.tmp")
if err != nil {
return
}
tmpPath := tmpFile.Name()

if err := tmpFile.Chmod(0644); err != nil {
tmpFile.Close()
os.Remove(tmpPath)
return
}
Comment on lines +147 to +151
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The temporary file is created in the system's temporary directory. Using 0644 permissions allows any local user to read the sparkline data. Since os.CreateTemp already creates the file with secure 0600 permissions, it is recommended to remove this Chmod call unless world-readability is explicitly required.


if _, err := tmpFile.Write(data); err != nil {
tmpFile.Close()
os.Remove(tmpPath)
return
}
Comment on lines +153 to +157
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure the atomic write is durable, tmpFile.Sync() should be called before closing the file. This guarantees that the data is flushed to stable storage before the rename operation occurs, preventing data loss or corruption in the event of a system crash.

Suggested change
if _, err := tmpFile.Write(data); err != nil {
tmpFile.Close()
os.Remove(tmpPath)
return
}
if _, err := tmpFile.Write(data); err != nil {
tmpFile.Close()
os.Remove(tmpPath)
return
}
if err := tmpFile.Sync(); 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)
return
}
os.Rename(tmp, path)
}

// PushAndSave is a convenience that loads, pushes, saves, and returns the buffer
Expand Down
Loading