Skip to content

Path traversal in KMS remove_cache #558

@pbeza

Description

@pbeza

The KMS app key endpoint in dstack/kms/src/main_service.rs accepts a caller-supplied app_id path component without sanitizing for directory traversal characters, potentially allowing access to keys belonging to other apps.

Root Cause

The remove_cache admin endpoint joins a user-supplied sub_dir parameter with a parent directory path without sanitizing path traversal sequences (../). While the endpoint requires an admin token, an attacker who has obtained the admin token (e.g., via a timing side-channel) can use this to read or delete arbitrary files on the KMS filesystem.

// main_service.rs:122-137
let path = parent_dir.join(sub_dir);  // sub_dir not sanitized
fs::remove_dir_all(&path)?;

Attack Path

  1. Attacker obtains the KMS admin token (e.g., via timing attack on a related vulnerability, or from leaked config)
  2. Attacker calls remove_cache with sub_dir = "../../etc" or similar traversal path
  3. The unsanitized path resolves to a directory outside the intended cache directory
  4. fs::remove_dir_all deletes the traversed path
  5. Attacker can delete KMS state files, configuration, or root key storage

Impact

Arbitrary file/directory deletion on the KMS filesystem. An attacker with the admin token can destroy KMS state, including root keys, configuration, and cached certificates. This could cause permanent data loss or denial of service for all CVMs depending on the KMS.

Suggested Fix

Sanitize the sub_dir parameter to prevent path traversal. Reject absolute paths and strip non-normal components, then verify the resolved path is within the expected directory:

let sub_path = std::path::Path::new(sub_dir);
if sub_path.is_absolute() {
    return Err(Error::InvalidPath);
}

// Strip ".." and other non-normal components
let cleaned: std::path::PathBuf = sub_path
    .components()
    .filter_map(|c| match c {
        std::path::Component::Normal(part) => Some(part),
        _ => None,
    })
    .collect();

let path = parent_dir.join(&cleaned);
let canonical_parent = parent_dir.canonicalize()?;

// Verify resolved path is within the expected directory.
// Use canonical parent + cleaned path as fallback if target doesn't exist yet.
let canonical = path
    .canonicalize()
    .unwrap_or_else(|_| canonical_parent.join(&cleaned));
if !canonical.starts_with(&canonical_parent) {
    return Err(Error::InvalidPath);
}

Note: This issue was created automatically. The vulnerability report was generated by Claude and has not been verified by a human.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions