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
7 changes: 7 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -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`.
22 changes: 20 additions & 2 deletions internal/burnrate/burnrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +80 to 87
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

To avoid a potential TOCTOU (Time-of-check to time-of-use) race condition, it is safer to use tmpFile.Chmod(0644) on the open file descriptor before closing it, rather than calling os.Chmod(tmpPath, 0644) on the file path after it has been closed. This ensures the permissions are applied to the exact file that was created, which is consistent with the security goals of this PR.

Suggested change
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 := tmpFile.Chmod(0644); 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
25 changes: 23 additions & 2 deletions internal/sparkline/sparkline.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +156 to 164
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

Similar to the improvement in burnrate.go, using tmpFile.Chmod(0644) before closing the file handle is more secure than calling os.Chmod on the path after closing, as it prevents potential manipulation of the file path between the two operations.

Suggested change
if err := tmpFile.Close(); err != nil {
os.Remove(tmp)
return
}
if err := os.Chmod(tmp, 0644); err != nil {
os.Remove(tmp)
return
}
if err := tmpFile.Chmod(0644); err != nil {
tmpFile.Close()
os.Remove(tmp)
return
}
if err := tmpFile.Close(); err != nil {
os.Remove(tmp)
return
}


os.Rename(tmp, path)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The error returned by os.Rename is currently ignored. If the rename operation fails (e.g., due to a file lock or permission issue), the temporary file will remain on disk and the data will not be persisted. The error should be handled, and the temporary file should be removed on failure to ensure a clean state.

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

}

Expand Down
Loading