Skip to content

DK26/strict-path-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

strict-path

Crates.io Documentation License: MIT OR Apache-2.0 CI Security Audit Type-State Police

πŸ“š Complete Guide & Examples | πŸ“– API Docs

Prevent directory traversal with type-safe path restriction and safe symlinks.

The Type-State Police have set up PathBoundary checkpoints
keeping your unruly paths in line, because your LLM is running wild

🚨 One Line of Code Away from Disaster

// ❌ This single line can destroy your server
std::fs::write(user_input, data)?;  // user_input = "../../../etc/passwd"

// βœ… This single line makes it mathematically impossible  
PathBoundary::try_new("uploads")?.strict_join(user_input)?.write_bytes(data)?;

The Reality: Every web server, LLM agent, and file processor faces the same vulnerability. One unvalidated path from user input, config files, or AI responses can grant attackers full filesystem access.

The Solution: Comprehensive path security with mathematical guarantees. No more hoping you "got it right."

πŸ›‘οΈ How We Solve The Entire Problem Class

strict-path isn't just validationβ€”it's a complete solution to path security:

  1. πŸ”§ soft-canonicalize foundation: Heavily tested against 19+ globally known path-related CVEs
  2. 🚫 Hacky string rejection: Advanced pattern detection blocks encoding tricks and malformed inputs
  3. πŸ“ Mathematical correctness: Rust's type system provides compile-time proof of path boundaries
  4. πŸ‘οΈ Explicit operations: Method names like strict_join() make security violations visible in code review
  5. πŸ€– LLM-aware design: Built specifically for untrusted AI-generated paths and modern threat models
  6. πŸ”— Symlink resolution: Safe handling of symbolic links with cycle detection and boundary enforcement
  7. ⚑ Dual protection modes: Choose Strict (validate & reject) or Virtual (clamp & contain) based on your use case
  8. πŸ—οΈ Battle-tested architecture: Prototyped and refined across real-world production systems
  9. 🎯 Zero-allocation interop: Seamless integration with existing std::path ecosystems

Recently Addressed CVEs

  • CVE-2025-8088 (WinRAR ADS): NTFS Alternate Data Stream traversal prevention
  • CVE-2022-21658 (TOCTOU): Race condition protection during path resolution
  • CVE-2019-9855, CVE-2020-12279, CVE-2017-17793: Windows 8.3 short name vulnerabilities

Your security audit becomes: "We use strict-path for comprehensive path security." βœ…

⚑ Get Secure in 30 Seconds

[dependencies]
strict-path = "0.1.0-alpha.2"
use strict_path::PathBoundary;

// 1. Create a boundary (your security perimeter)
let uploads = PathBoundary::try_new_create("uploads")?;

// 2. ANY external input becomes safe 
let safe_path = uploads.strict_join(dangerous_user_input)?;  // Attack = Error

// 3. Use normal file operations - guaranteed secure
safe_path.write_bytes(file_data)?;
safe_path.read_to_string()?;

That's it. No complex validation logic. No CVE research. No security expertise required.

πŸ›‘οΈ Security Features

  • CVE-Aware Protection: Built on 19+ real-world vulnerabilities - we've done the security research so you don't have to
  • Mathematical Guarantees: Paths are canonicalized and boundary-checked - impossible to escape the restriction
  • Type Safety: Marker types prevent mixing different storage contexts at compile time
  • LLM-Ready: Designed specifically for untrusted AI-generated paths and modern threat models
  • Platform Security: Handles Windows 8.3 short names, symlinks, and other OS-specific attack vectors
  • Zero-Allocation Interop: .interop_path() for seamless integration with existing std::path code
  • Misuse Resistant: API design makes security violations visible in code review

🎯 When to Use Each Type

Your Input Source Use This Why
HTTP requests, LLM output, config files StrictPath Reject attacks explicitly - perfect for validation
User uploads, archive extraction VirtualPath Clamp hostile paths safely - perfect for sandboxing
Your own hardcoded paths Path/PathBuf You control it, no validation needed

Think of it this way:

  • StrictPath = Security Filter β€” validates and rejects unsafe paths
  • VirtualPath = Complete Sandbox β€” clamps any input to stay safe

πŸ›‘οΈ Core Security Foundation

At the heart of this crate is StrictPath - the fundamental security primitive that provides our ironclad guarantee: every StrictPath is mathematically proven to be within its boundary.

Everything in this crate builds upon StrictPath:

  • PathBoundary creates and validates StrictPath instances
  • VirtualPath extends StrictPath with user-friendly virtual root semantics
  • VirtualRoot provides a root context for creating VirtualPath instances

The core promise: If you have a StrictPath<Marker>, it is impossible for it to reference anything outside its designated boundary. This isn't just validation - it's a type-level guarantee backed by cryptographic-grade path canonicalization.

Core Security Principle: Secure Every External Path

Any path from untrusted sources (HTTP, CLI, config, DB, LLMs, archives) must be validated into a boundary‑enforced type (StrictPath or VirtualPath) before I/O.

🎯 Choose Your Weapon: When to Use What

🌐 VirtualPath - User Sandboxes & Cloud Storage

"Give users their own private universe"

use strict_path::VirtualRoot;

// Archive extraction - hostile names get clamped, not rejected
let extract_dir = VirtualRoot::try_new("./extracted")?;
for entry_name in malicious_zip_entries {
    let safe_path = extract_dir.virtual_join(entry_name)?; // "../../../etc" β†’ "/etc"  
    safe_path.write_bytes(entry.data())?; // Always safe
}

// User cloud storage - users see friendly paths
let user_space = VirtualRoot::try_new(format!("users/{user_id}"))?;
let doc = user_space.virtual_join("My Documents/report.pdf")?;
println!("Saved to: {}", doc.virtualpath_display()); // Shows "/My Documents/report.pdf"

βš”οΈ StrictPath - LLM Agents & System Boundaries

"Validate everything, trust nothing"

use strict_path::PathBoundary;

// LLM Agent file operations
let ai_workspace = PathBoundary::try_new("ai_sandbox")?;
let ai_request = llm.generate_path(); // Could be anything malicious
let safe_path = ai_workspace.strict_join(ai_request)?; // Attack β†’ Explicit Error
safe_path.write_string(&ai_generated_content)?;

// Limited system access with clear boundaries
struct ConfigFiles; 
let config_dir = PathBoundary::<ConfigFiles>::try_new("./config")?;
let user_config = config_dir.strict_join(user_selected_config)?; // Validated

πŸ”“ Path/PathBuf - Controlled Access

"When you control the source"

use std::path::PathBuf;

// βœ… You control the input - no validation needed
let log_file = PathBuf::from(format!("logs/{}.log", timestamp));
let app_config = Path::new("config/app.toml"); // Hardcoded = safe

// ❌ NEVER with external input
let user_file = Path::new(user_input); // 🚨 SECURITY DISASTER

πŸŽ–οΈ The Golden Rule

If you didn't create the path yourself, secure it first.

Input Source Use This Why
HTTP requests, CLI args, config files StrictPath Reject attacks explicitly
LLM/AI output, database records StrictPath Validate before execution
Archive contents, user uploads VirtualPath Clamp hostile paths safely
Your own code, hardcoded paths Path/PathBuf You control it

πŸš€ Real-World Examples

LLM Agent File Manager

use strict_path::PathBoundary;

// Encode guarantees in signature: pass workspace boundary and untrusted request
async fn llm_file_operation(workspace: &PathBoundary, request: &LlmRequest) -> Result<String> {
    // LLM could suggest anything: "../../../etc/passwd", "C:/Windows/System32", etc.
    let safe_path = workspace.strict_join(&request.filename)?; // Attack = Error

    match request.operation.as_str() {
        "write" => safe_path.write_string(&request.content)?,
        "read" => return Ok(safe_path.read_to_string()?),
        _ => return Err("Invalid operation".into()),
    }
    Ok(format!("File {} processed safely", safe_path.strictpath_display()))
}

Zip Extraction (Zip Slip Prevention)

use strict_path::VirtualRoot;

// Encode guarantees in signature: pass the extract root and untrusted entry names
fn extract_zip(zip_entries: impl IntoIterator<Item=(String, Vec<u8>)>, extract_root: &VirtualRoot) -> std::io::Result<()> {
    for (name, data) in zip_entries {
        // Hostile names like "../../../etc/passwd" get clamped to "/etc/passwd"
        let vpath = extract_root.virtual_join(&name)?;
        vpath.create_parent_dir_all()?;
        vpath.write_bytes(&data)?;
    }
    Ok(())
}

Web File Server

use strict_path::PathBoundary;

struct StaticFiles;

async fn serve_static(static_dir: &PathBoundary<StaticFiles>, path: &str) -> Result<Response> {
    let safe_path = static_dir.strict_join(path)?; // "../../../" β†’ Error
    Ok(Response::new(safe_path.read_bytes()?))
}

// Function signature prevents bypass - no validation needed inside!
async fn serve_file(safe_path: &strict_path::StrictPath<StaticFiles>) -> Response {
    Response::new(safe_path.read_bytes().unwrap_or_default())
}

Configuration Manager

use strict_path::PathBoundary;

struct UserConfigs;

fn load_user_config(config_dir: &PathBoundary<UserConfigs>, config_name: &str) -> Result<Config> {
    let config_file = config_dir.strict_join(config_name)?;
    Ok(serde_json::from_str(&config_file.read_to_string()?)?)
}

⚠️ Security Scope

What this protects against (99% of attacks):

  • Path traversal (../../../etc/passwd)
  • Symlink escapes and directory bombs
  • Archive extraction attacks (zip slip)
  • Unicode/encoding bypass attempts
  • Windows-specific attacks (8.3 names, UNC paths)
  • Race conditions during path resolution

What requires system-level privileges (rare):

  • Hard links: Multiple filesystem entries to same file data
  • Mount points: Admin/root can redirect paths via filesystem mounts

Bottom line: If attackers have root/admin access, they've already won. This library stops the 99% of practical attacks that don't require special privileges.

πŸ“‹ Input Source Decision Matrix

Source Typical Input Use VirtualPath For Use StrictPath For Notes
🌐 HTTP requests URL path segments, file names Display/logging, safe virtual joins System-facing interop/I/O Always clamp user paths via VirtualRoot::virtual_join
🌍 Web forms Form file fields, route params User-facing display, UI navigation System-facing interop/I/O Treat all form inputs as untrusted
βš™οΈ Configuration files Paths in config UI display and I/O within boundary System-facing interop/I/O Validate each path before I/O
πŸ’Ύ Database content Stored file paths Rendering paths in UI dashboards System-facing interop/I/O Storage does not imply safety; validate on use
πŸ“‚ CLI arguments Command-line path args Pretty printing, I/O within boundary System-facing interop/I/O Validate args before touching filesystem
πŸ”Œ External APIs Webhooks, 3rd-party payloads Present sanitized paths to logs System-facing interop/I/O Never trust external systems
πŸ€– LLM/AI output Generated file names/paths Display suggestions, I/O within boundary System-facing interop/I/O LLM output is untrusted by default
πŸ“¨ Inter-service msgs Queue/event payloads Observability output, I/O within boundary System-facing interop/I/O Validate on the consumer side
πŸ“± Apps (desktop/mobile) Drag-and-drop, file pickers Show picked paths in UI System-facing interop/I/O Validate selected paths before I/O
πŸ“¦ Archive contents Entry names from ZIP/TAR Progress UI, virtual joins System-facing interop/I/O Validate each entry to block zip-slip
πŸ”§ File format internals Embedded path strings Diagnostics, I/O within boundary System-facing interop/I/O Never dereference without validation

Note: This is not β€œStrictPath vs VirtualPath.” VirtualPath conceptually extends StrictPath with a virtual-root view and restricted, path boundary-aware operations. Both support I/O and interop; choose based on whether you need virtual, user-facing path semantics or raw system-facing semantics.

Think of it this way:

  • StrictPath = Security Filter β€” validates and rejects unsafe paths
  • VirtualPath = Complete Sandbox β€” clamps any input to stay safe

Unified Signatures (When Appropriate): Prefer marker-specific &StrictPath<Marker> for stronger guarantees. Use a generic &StrictPath<_> only when the function is intentionally shared across contexts; call with vpath.as_unvirtual() when starting from a VirtualPath.

use strict_path::{PathBoundary, VirtualRoot};

fn process_file<M>(path: &strict_path::StrictPath<M>) -> std::io::Result<Vec<u8>> {
  path.read_bytes()
}

// Call with either type
let boundary = PathBoundary::try_new("directory")?;
let spath = boundary.strict_join("file.txt")?;
process_file(&spath)?;

let vroot = VirtualRoot::try_new("directory")?;
let vpath = vroot.virtual_join("file.txt")?;
process_file(vpath.as_unvirtual())?;

πŸ” Advanced: Type-Safe Context Separation

Use markers to prevent mixing different storage contexts at compile time:

use strict_path::{PathBoundary, StrictPath, VirtualRoot, VirtualPath};

struct WebAssets;    // CSS, JS, images  
struct UserFiles;    // Uploaded documents

// Functions enforce context via type system
fn serve_asset(path: &StrictPath<WebAssets>) -> Response { /* ... */ }
fn process_upload(path: &StrictPath<UserFiles>) -> Result<()> { /* ... */ }

// Create context-specific boundaries
let assets_root: VirtualRoot<WebAssets> = VirtualRoot::try_new("public")?;
let uploads_root: VirtualRoot<UserFiles> = VirtualRoot::try_new("uploads")?;

let css: VirtualPath<WebAssets> = assets_root.virtual_join("app.css")?;
let doc: VirtualPath<UserFiles> = uploads_root.virtual_join("report.pdf")?;

// Type system prevents context mixing
serve_asset(&css.unvirtual());         // βœ… Correct context
// serve_asset(&doc.unvirtual());      // ❌ Compile error!

Your IDE and compiler become security guards.

App Configuration with app_path:

// ❌ Vulnerable - app dirs + user paths
use app_path::AppPath;
let app_dir = AppPath::new("MyApp").get_app_dir();
let config_file = app_dir.join(user_config_name); // 🚨 Potential escape
fs::write(config_file, settings)?;

// βœ… Protected - bounded app directories  
use strict_path::PathBoundary;
let boundary = PathBoundary::try_new_create(AppPath::new("MyApp").get_app_dir())?;
let safe_config = boundary.strict_join(user_config_name)?; // βœ… Validated
safe_config.write_string(&settings)?;

⚠️ Anti-Patterns (Tell‑offs and Fixes)

DON'T Mix Interop with Display

use strict_path::PathBoundary;
let boundary = PathBoundary::try_new("uploads")?;

// ❌ ANTI-PATTERN: Wrong method for display
println!("Path: {}", boundary.interop_path().to_string_lossy());

// βœ… CORRECT: Use proper display methods
println!("Path: {}", boundary.strictpath_display());

// For VirtualRoot, access the underlying StrictPath:
use strict_path::VirtualRoot;
let vroot = VirtualRoot::try_new("uploads")?;
println!("Path: {}", vroot.as_unvirtual().strictpath_display());

Why this matters:

  • interop_path() is designed for external API interop (AsRef<Path>)
  • *_display() methods are designed for human-readable output
  • Mixing concerns makes code harder to understand and maintain

Web Server File Serving

struct StaticFiles; // Marker for static assets

async fn serve_static_file(safe_path: &StrictPath<StaticFiles>) -> Result<Response> {
    // Function signature enforces safety - no validation needed inside!
    Ok(Response::new(safe_path.read_bytes()?))
}

// Caller handles validation once:
let static_files_dir = PathBoundary::<StaticFiles>::try_new("./static")?;
let safe_path = static_files_dir.strict_join(&user_requested_path)?;
serve_static_file(&safe_path).await?;

Archive Extraction (Zip Slip Prevention)

let extract_root = VirtualRoot::try_new("./extracted")?;
for (name, data) in zip_entries {
    let vpath = extract_root.virtual_join(&name)?;  // Neutralizes zip slip (clamps hostile)
    vpath.create_parent_dir_all()?;
    vpath.write_bytes(&data)?;
}

Cloud Storage API

// User chooses any path - always safe
let user_storage = VirtualRoot::try_new(format!("/cloud/user_{id}"))?;
let file_path = user_storage.virtual_join(&user_requested_path)?;
file_path.write_bytes(upload_data)?;

Configuration Files

use strict_path::PathBoundary;

// Encode guarantees via the signature: pass the boundary and an untrusted name
fn load_config(config_dir: &PathBoundary, name: &str) -> Result<String> {
    config_dir.strict_join(name)?.read_to_string()
}
  • Constructing boundaries/roots inside helpers
    • Helpers shouldn’t decide policy. Take a &PathBoundary/&VirtualRoot and a name, or accept a &StrictPath/&VirtualPath.
  • Wrapping secure types in std paths
    • Don’t wrap interop_path() in Path::new/PathBuf::from; pass interop_path() directly to AsRef<Path> APIs.

LLM/AI File Operations

// AI suggests file operations - always validated
let ai_workspace = PathBoundary::try_new("ai_workspace")?;
let ai_suggested_path = llm_generate_filename(); // Could be anything!
let safe_ai_path = ai_workspace.strict_join(ai_suggested_path)?; // Guaranteed safe
safe_ai_path.write_string(&ai_generated_content)?;

πŸ“š API Quick Reference

Feature Path/PathBuf StrictPath VirtualPath
Security None πŸ’₯ Validates & rejects βœ… Clamps any input βœ…
Join safety Unsafe (can escape) Boundary-checked Boundary-clamped
Example attack "../../../etc/passwd" β†’ System breach "../../../etc/passwd" β†’ Error "../../../etc/passwd" β†’ /etc/passwd (safe)
Best for Known-safe paths System boundaries User interfaces
// StrictPath - validate and reject
let boundary = PathBoundary::try_new("uploads")?;
let path = boundary.strict_join("file.txt")?; // Error if unsafe

// VirtualPath - clamp any input safely  
let vroot = VirtualRoot::try_new("userspace")?;
let vpath = vroot.virtual_join("any/path/here")?; // Always works

// Both support the same I/O operations
path.write_bytes(data)?;
vpath.read_to_string()?;

πŸ“š Documentation & Resources

πŸ”Œ Integrations

  • πŸ—‚οΈ OS Directories (dirs feature): PathBoundary::try_new_os_config(), try_new_os_downloads(), etc.
  • πŸ“„ Serde (serde feature): Safe serialization/deserialization of path types
  • 🌐 Axum: Custom extractors for web servers (see demos/ for examples)

πŸ“„ License

MIT OR Apache-2.0