-
Notifications
You must be signed in to change notification settings - Fork 4
π‘οΈ Sentinel: [CRITICAL] Fix symlink vulnerability in atomic file writes #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| ## 2026-05-03 - Symlink Vulnerability in Atomic File Writes | ||
| **Vulnerability:** Predictable temporary filenames used before `os.Rename` are vulnerable to symlink attacks. | ||
| **Learning:** Atomic file writes often use a predictable temporary file (e.g., appending `.tmp` or `.new`), which an attacker can pre-create as a symlink to an arbitrary target. | ||
| **Prevention:** Use `os.CreateTemp` to generate unpredictable temporary filenames in the same directory as the target, write data, close the file, and then rename. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 { | ||||||||||||||||||||||||||||||||||||||
| tmpFile, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)+".*") | ||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| tmp := tmpFile.Name() | ||||||||||||||||||||||||||||||||||||||
| if _, err := tmpFile.Write(data); err != nil { | ||||||||||||||||||||||||||||||||||||||
| tmpFile.Close() | ||||||||||||||||||||||||||||||||||||||
| os.Remove(tmp) | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| os.Rename(tmp, path) | ||||||||||||||||||||||||||||||||||||||
| tmpFile.Chmod(0644) | ||||||||||||||||||||||||||||||||||||||
| tmpFile.Close() | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+146
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The errors from
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| if err := os.Rename(tmp, path); err != nil { | ||||||||||||||||||||||||||||||||||||||
| os.Remove(tmp) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // PushAndSave is a convenience that loads, pushes, saves, and returns the buffer | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,7 +68,6 @@ func Download(ctx context.Context) error { | |
| return fmt.Errorf("failed to get home directory: %w", err) | ||
| } | ||
| binaryPath := filepath.Join(homeDir, ".claude", "prism") | ||
| tempPath := binaryPath + ".new" | ||
|
|
||
| // Download to temp file | ||
| req, err := http.NewRequestWithContext(ctx, "GET", binaryURL, nil) | ||
|
|
@@ -91,10 +90,11 @@ func Download(ctx context.Context) error { | |
| } | ||
|
|
||
| // Write to temp file | ||
| out, err := os.Create(tempPath) | ||
| out, err := os.CreateTemp(filepath.Dir(binaryPath), filepath.Base(binaryPath)+".*") | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create temp file: %w", err) | ||
| } | ||
| tempPath := out.Name() | ||
|
|
||
| _, err = io.Copy(out, resp.Body) | ||
| out.Close() | ||
|
Comment on lines
99
to
100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error from _, err = io.Copy(out, resp.Body)
if cerr := out.Close(); err == nil {
err = cerr
} |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The errors from
tmpFile.Chmod(0644)andtmpFile.Close()are currently ignored. For reliable atomic writes, it is important to check the error returned byClose()because this is when the operating system flushes remaining data to disk and reports any write failures (e.g., disk full). Additionally, for true durability, consider callingtmpFile.Sync()before closing to ensure the data is persisted to the physical storage before the rename operation occurs.