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-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`.
14 changes: 12 additions & 2 deletions internal/burnrate/burnrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment on lines +73 to +79
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 and complete, it is recommended to call f.Sync() before closing the file, and to check the error returned by f.Close(). If f.Close() fails, it may indicate that the data was not successfully flushed to disk, and the subsequent os.Rename could result in a corrupted or incomplete file.

	if _, err := f.Write(data); err != nil {
		f.Close()
		os.Remove(tmpPath)
		return nil, false, err
	}
	if err := f.Sync(); 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
Expand Down
16 changes: 13 additions & 3 deletions internal/sparkline/sparkline.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment on lines +146 to +152
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

Consider adding f.Sync() and checking the error from f.Close() to ensure data durability and integrity before renaming the temporary file. This ensures that the file is fully written to disk before it replaces the target file.

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

if err := os.Rename(tmp, path); err != nil {
os.Remove(tmp)
}
}

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