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 @@
## 2025-01-20 - Predictable Temporary Filenames Vulnerability
**Vulnerability:** Found predictable temporary filenames being used during atomic file writes (`path + ".tmp"`) in `os.TempDir()` within `internal/burnrate/burnrate.go` and `internal/sparkline/sparkline.go`.
**Learning:** Atomic file writes often use a predictable `.tmp` suffix to create a temporary file before renaming. In shared directories like `os.TempDir()`, this predictable naming allows attackers to pre-create the file as a symlink, tricking the application into overwriting unintended files (Symlink attack).
**Prevention:** Always use `os.CreateTemp` with an unpredictable pattern (e.g., `filepath.Base(path)+"-*"`) for temporary files. Ensure the file is created in the same directory as the target (`filepath.Dir(path)`), explicitly set permissions with `f.Chmod` if needed, and call `f.Close()` before renaming to prevent cross-platform issues.
18 changes: 16 additions & 2 deletions internal/burnrate/burnrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,22 @@ 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), filepath.Base(path)+"-*")
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.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 {
Expand Down
22 changes: 19 additions & 3 deletions internal/sparkline/sparkline.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,27 @@ 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), filepath.Base(path)+"-*")
if err != nil {
return
}
tmp := f.Name()
if _, err := f.Write(data); err != nil {
f.Close()
os.Remove(tmp)
return
}
if err := f.Close(); err != nil {
os.Remove(tmp)
return
}
if err := os.Chmod(tmp, 0644); err != nil {
os.Remove(tmp)
return
}
os.Rename(tmp, path)
if err := os.Rename(tmp, path); err != nil {
os.Remove(tmp)
}
Comment on lines +145 to +161
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

Similar to the changes in burnrate.go, using f.Chmod and defer os.Remove would make the code more idiomatic and robust. This reduces boilerplate and ensures the temporary file is cleaned up on all error paths, including the final rename.

Suggested change
tmp := f.Name()
if _, err := f.Write(data); err != nil {
f.Close()
os.Remove(tmp)
return
}
if err := f.Close(); err != nil {
os.Remove(tmp)
return
}
if err := os.Chmod(tmp, 0644); err != nil {
os.Remove(tmp)
return
}
os.Rename(tmp, path)
if err := os.Rename(tmp, path); err != nil {
os.Remove(tmp)
}
tmp := f.Name()
defer os.Remove(tmp)
if _, err := f.Write(data); err != nil {
f.Close()
return
}
if err := f.Chmod(0644); err != nil {
f.Close()
return
}
if err := f.Close(); err != nil {
return
}
if err := os.Rename(tmp, path); err != nil {
return
}

}

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