Skip to content

Commit bbb8928

Browse files
committed
fix(tui): use secure random temp files in external editor to prevent symlink attacks
The external editor was using predictable temp filenames based on PID (cortex_prompt_{PID}.md), making it vulnerable to symlink attacks where an attacker could: 1. Predict the filename before cortex creates it 2. Create a symlink pointing to a sensitive file 3. When cortex writes to the temp file, it overwrites the symlink target This fix uses the tempfile crate which: - Creates files with cryptographically random names (16 random bytes) - Uses O_EXCL flag to fail if file exists (preventing TOCTOU races) - Sets restrictive permissions (0600 on Unix) Changes: - Replace predictable PID-based filenames with random tempfile names - Use tempfile::Builder for secure temp file creation - Update both async and sync versions of open_external_editor - Add tempfile as a runtime dependency (was already dev dependency) Security Impact: Prevents local privilege escalation via symlink attacks on temp files. Fixes: issue #5405
1 parent c398212 commit bbb8928

File tree

2 files changed

+32
-14
lines changed

2 files changed

+32
-14
lines changed

src/cortex-tui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ walkdir = { workspace = true }
6565

6666
# External editor
6767
which = { workspace = true }
68+
tempfile = { workspace = true }
6869

6970
# Audio notifications
7071
rodio = { workspace = true }

src/cortex-tui/src/external_editor.rs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -162,17 +162,25 @@ pub async fn open_external_editor(initial_content: &str) -> Result<String, Edito
162162
// Get the editor command
163163
let editor_cmd = get_editor()?;
164164

165-
// Create a temporary file
166-
let temp_dir = std::env::temp_dir();
167-
let temp_file = temp_dir.join(format!("cortex_prompt_{}.md", std::process::id()));
168-
169-
// Write initial content
165+
// Create a temporary file with a secure random name to prevent symlink attacks.
166+
// Using tempfile crate ensures proper security (O_EXCL, restricted permissions).
167+
let temp_file = tempfile::Builder::new()
168+
.prefix("cortex_prompt_")
169+
.suffix(".md")
170+
.rand_bytes(16)
171+
.tempfile()
172+
.map_err(EditorError::Io)?;
173+
174+
// Write initial content using the secure file handle
170175
{
171-
let mut file = std::fs::File::create(&temp_file)?;
176+
let mut file = temp_file.reopen().map_err(EditorError::Io)?;
172177
file.write_all(initial_content.as_bytes())?;
173178
file.flush()?;
174179
}
175180

181+
// Keep the temp file alive (don't let it be deleted yet)
182+
let temp_file = temp_file.into_temp_path();
183+
176184
// Parse the editor command
177185
let parts: Vec<&str> = editor_cmd.split_whitespace().collect();
178186
let (editor, args) = match parts.split_first() {
@@ -219,17 +227,25 @@ pub fn open_external_editor_sync(initial_content: &str) -> Result<String, Editor
219227
// Get the editor command
220228
let editor_cmd = get_editor()?;
221229

222-
// Create a temporary file
223-
let temp_dir = std::env::temp_dir();
224-
let temp_file = temp_dir.join(format!("cortex_prompt_{}.md", std::process::id()));
230+
// Create a temporary file with a secure random name to prevent symlink attacks.
231+
// Using tempfile crate ensures proper security (O_EXCL, restricted permissions).
232+
let temp_file = tempfile::Builder::new()
233+
.prefix("cortex_prompt_")
234+
.suffix(".md")
235+
.rand_bytes(16)
236+
.tempfile()
237+
.map_err(EditorError::Io)?;
225238

226-
// Write initial content
239+
// Write initial content using the secure file handle
227240
{
228-
let mut file = std::fs::File::create(&temp_file)?;
241+
let mut file = temp_file.reopen().map_err(EditorError::Io)?;
229242
file.write_all(initial_content.as_bytes())?;
230243
file.flush()?;
231244
}
232245

246+
// Keep the temp file alive (don't let it be deleted yet)
247+
let temp_file = temp_file.into_temp_path();
248+
233249
// Parse the editor command
234250
let parts: Vec<&str> = editor_cmd.split_whitespace().collect();
235251
let (editor, args) = match parts.split_first() {
@@ -264,12 +280,13 @@ pub fn open_external_editor_sync(initial_content: &str) -> Result<String, Editor
264280
Ok(content.trim().to_string())
265281
}
266282

267-
/// Gets the path to the temporary file that would be used.
283+
/// Gets an example path pattern for temporary files.
268284
///
269-
/// Useful for displaying to the user.
285+
/// Note: Actual temp files use random suffixes for security.
286+
/// This function returns a pattern showing the general location.
270287
pub fn get_temp_file_path() -> PathBuf {
271288
let temp_dir = std::env::temp_dir();
272-
temp_dir.join(format!("cortex_prompt_{}.md", std::process::id()))
289+
temp_dir.join("cortex_prompt_XXXXXXXXXXXXXXXX.md")
273290
}
274291

275292
// ============================================================

0 commit comments

Comments
 (0)