Project
cortex
Description
cortex-snapshot/src/storage.rs SnapshotStorage uses session_id and snapshot_id directly in path construction without any sanitization:
fn snapshot_path(&self, id: &str) -> PathBuf { self.storage_path.join(format!('{}.json', id)) }
fn history_path(&self, session_id: &str) -> PathBuf { self.storage_path.join(format!('history_{}.json', session_id)) }
If session_id contains '../' sequences (e.g., '../../../etc/cron.d/evil'), the resulting path traverses outside the intended storage directory.
Rust's Path::join() does NOT sanitize '../' components: '/data/snapshot_meta'.join('history_../../etc/cron.d/evil.json') => '/data/snapshot_meta/history_../../etc/cron.d/evil.json' => which resolves to '/etc/cron.d/evil.json' via the OS
ATTACK VECTORS: save_revert_history('../../../etc/cron.d/backdoor', history): writes arbitrary JSON to /etc/cron.d/backdoor.json load_snapshot('../../etc/passwd'): reads /etc/passwd (mapped as serde_json parse error -> file not found)
Cortex processes user-specified session names from API calls or CLI args. If a session name is user-controlled and passed to storage functions, arbitrary files can be written or read outside the snapshot directory.
Error Message
Debug Logs
cortex-snapshot/src/storage.rs:135-142 (UNSANITIZED PATH CONSTRUCTION):
fn snapshot_path(&self, id: &str) -> PathBuf {
// BUG: id is directly inserted into path with no sanitization!
self.storage_path.join(format!("{}.json", id))
}
fn history_path(&self, session_id: &str) -> PathBuf {
// BUG: session_id is directly inserted into path!
self.storage_path.join(format!("history_{}.json", session_id))
}
Rust Path::join() does NOT prevent path traversal:
PathBuf::from("/data/snapshot_meta")
.join("history_../../etc/passwd.json")
== PathBuf::from("/data/snapshot_meta/history_../../etc/passwd.json")
When the OS resolves this path (open, write, etc.):
/data/snapshot_meta/history_../../etc/passwd.json
= /data/etc/passwd.json (if /data/snapshot_meta -> /data)
= /etc/passwd.json (if /data is /, traversal goes above root dir)
save_revert_history('../../tmp/evil', data)
-> path = /snapshot_meta/history_../../tmp/evil.json
-> writes to /tmp/evil.json (ARBITRARY FILE WRITE!)
System Information
OS: Ubuntu 22.04 LTS | Version: v0.0.7
Screenshots
https://github.com/petar-vasilev/screenshots/blob/main/b2c6e89520b542e2bc10151ce3c74000.png
Steps to Reproduce
- Run:
cortex snapshot list --session '../../etc/cron.d/backdoor'
- cortex session create --name '../../tmp/injected'
- Cortex creates snapshot, calls save_revert_history('../../tmp/injected', ...)
- File is written to /tmp/injected.json instead of snapshot_meta/
- cortex snapshot list --session '../../tmp/injected'
- File is read from /tmp/injected.json (path traversal read)
Expected Behavior
Sanitize IDs before using them in path construction. The simplest fix is to reject IDs containing path separators:
fn sanitize_id(id: &str) -> Result<()> {
if id.contains('/') || id.contains('\') || id.contains('.') {
// Check for path traversal components:
if Path::new(id).components().any(|c| matches!(c, Component::ParentDir)) {
return Err(SnapshotError::InvalidId(id.to_string()));
}
}
Ok(())
}
fn snapshot_path(&self, id: &str) -> Result {
sanitize_id(id)?;
Ok(self.storage_path.join(format!("{}.json", id)))
}
Or use a hash of the ID as the filename instead of the raw ID:
fn snapshot_path(&self, id: &str) -> PathBuf {
let safe_name = hex::encode(sha256(id.as_bytes()));
self.storage_path.join(format!("{}.json", safe_name))
}
Actual Behavior
cortex-snapshot/src/storage.rs:135-142: Both snapshot_path() and history_path() use the raw id/session_id strings directly in format!() for path construction, without checking for path traversal sequences. Rust's Path::join() allows '../' components. If these IDs are ever derived from user input (session names, message IDs), an attacker can write arbitrary JSON files to any location accessible by the Cortex process.
Additional Context
── Code Evidence ────────────────────────────────────────────────────
cortex-snapshot/src/storage.rs:135-142 (BUG LOCATION)
fn snapshot_path(&self, id: &str) -> PathBuf {
self.storage_path.join(format!("{}.json", id)) // UNSAFE!
}
fn history_path(&self, session_id: &str) -> PathBuf {
self.storage_path.join(format!("history_{}.json", session_id)) // UNSAFE!
}
Rust Path::join() documentation:
'If path is absolute, it replaces the current path.'
'Otherwise, all path components are appended.'
--> '../' is NOT treated as an error, it IS appended and resolved by OS
Note: Path::join() with an ABSOLUTE path (starting with '/') replaces
the entire base path, making it even more severe. So:
save_revert_history('/etc/passwd', ...) writes to /etc/passwd.json
Project
cortex
Description
cortex-snapshot/src/storage.rs SnapshotStorage uses session_id and snapshot_id directly in path construction without any sanitization:
fn snapshot_path(&self, id: &str) -> PathBuf { self.storage_path.join(format!('{}.json', id)) }
fn history_path(&self, session_id: &str) -> PathBuf { self.storage_path.join(format!('history_{}.json', session_id)) }
If session_id contains '../' sequences (e.g., '../../../etc/cron.d/evil'), the resulting path traverses outside the intended storage directory.
Rust's Path::join() does NOT sanitize '../' components: '/data/snapshot_meta'.join('history_../../etc/cron.d/evil.json') => '/data/snapshot_meta/history_../../etc/cron.d/evil.json' => which resolves to '/etc/cron.d/evil.json' via the OS
ATTACK VECTORS: save_revert_history('../../../etc/cron.d/backdoor', history): writes arbitrary JSON to /etc/cron.d/backdoor.json load_snapshot('../../etc/passwd'): reads /etc/passwd (mapped as serde_json parse error -> file not found)
Cortex processes user-specified session names from API calls or CLI args. If a session name is user-controlled and passed to storage functions, arbitrary files can be written or read outside the snapshot directory.
Error Message
Debug Logs
cortex-snapshot/src/storage.rs:135-142 (UNSANITIZED PATH CONSTRUCTION): fn snapshot_path(&self, id: &str) -> PathBuf { // BUG: id is directly inserted into path with no sanitization! self.storage_path.join(format!("{}.json", id)) } fn history_path(&self, session_id: &str) -> PathBuf { // BUG: session_id is directly inserted into path! self.storage_path.join(format!("history_{}.json", session_id)) } Rust Path::join() does NOT prevent path traversal: PathBuf::from("/data/snapshot_meta") .join("history_../../etc/passwd.json") == PathBuf::from("/data/snapshot_meta/history_../../etc/passwd.json") When the OS resolves this path (open, write, etc.): /data/snapshot_meta/history_../../etc/passwd.json = /data/etc/passwd.json (if /data/snapshot_meta -> /data) = /etc/passwd.json (if /data is /, traversal goes above root dir) save_revert_history('../../tmp/evil', data) -> path = /snapshot_meta/history_../../tmp/evil.json -> writes to /tmp/evil.json (ARBITRARY FILE WRITE!)System Information
OS: Ubuntu 22.04 LTS | Version: v0.0.7Screenshots
https://github.com/petar-vasilev/screenshots/blob/main/b2c6e89520b542e2bc10151ce3c74000.png
Steps to Reproduce
cortex snapshot list --session '../../etc/cron.d/backdoor'Expected Behavior
Sanitize IDs before using them in path construction. The simplest fix is to reject IDs containing path separators:
fn sanitize_id(id: &str) -> Result<()> {
if id.contains('/') || id.contains('\') || id.contains('.') {
// Check for path traversal components:
if Path::new(id).components().any(|c| matches!(c, Component::ParentDir)) {
return Err(SnapshotError::InvalidId(id.to_string()));
}
}
Ok(())
}
fn snapshot_path(&self, id: &str) -> Result {
sanitize_id(id)?;
Ok(self.storage_path.join(format!("{}.json", id)))
}
Or use a hash of the ID as the filename instead of the raw ID:
fn snapshot_path(&self, id: &str) -> PathBuf {
let safe_name = hex::encode(sha256(id.as_bytes()));
self.storage_path.join(format!("{}.json", safe_name))
}
Actual Behavior
cortex-snapshot/src/storage.rs:135-142: Both snapshot_path() and history_path() use the raw id/session_id strings directly in format!() for path construction, without checking for path traversal sequences. Rust's Path::join() allows '../' components. If these IDs are ever derived from user input (session names, message IDs), an attacker can write arbitrary JSON files to any location accessible by the Cortex process.
Additional Context
── Code Evidence ────────────────────────────────────────────────────
cortex-snapshot/src/storage.rs:135-142 (BUG LOCATION)
fn snapshot_path(&self, id: &str) -> PathBuf {
self.storage_path.join(format!("{}.json", id)) // UNSAFE!
}
fn history_path(&self, session_id: &str) -> PathBuf {
self.storage_path.join(format!("history_{}.json", session_id)) // UNSAFE!
}
Rust Path::join() documentation:
'If path is absolute, it replaces the current path.'
'Otherwise, all path components are appended.'
--> '../' is NOT treated as an error, it IS appended and resolved by OS
Note: Path::join() with an ABSOLUTE path (starting with '/') replaces
the entire base path, making it even more severe. So:
save_revert_history('/etc/passwd', ...) writes to /etc/passwd.json