Skip to content

[BUG] [v0.0.7] cortex-snapshot SnapshotStorage path construction uses unsanitized IDs — path traversal via malicious session_id writes arbitrary JSON files outside storage directory #53119

@petar-vasilev

Description

@petar-vasilev

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

  1. Run: cortex snapshot list --session '../../etc/cron.d/backdoor'
  2. cortex session create --name '../../tmp/injected'
  3. Cortex creates snapshot, calls save_revert_history('../../tmp/injected', ...)
  4. File is written to /tmp/injected.json instead of snapshot_meta/
  5. cortex snapshot list --session '../../tmp/injected'
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingideIssues related to IDEvalidValid issue

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions