strict-path 0.1.0-beta.3

Enforce path boundaries with compile-time guarantees. Validates paths (StrictPath) or sandboxes them (VirtualPath). Handles symlinks, CVEs, platform quirks.
Documentation

strict-path

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

📚 Complete Guide & Examples | 📖 API Docs | 🧭 Choosing Canonicalized vs Lexical Solution

Strictly enforce path boundaries to prevent directory traversal attacks. This crate provides compile-time guarantees that file paths cannot escape their designated boundaries—no exceptions, no escapes.

Note: Our doc comments and LLM_API_REFERENCE.md are designed for LLMs with function calling—so an AI can use this crate safely and correctly for file and path operations.

🤖 LLM agent prompt (copy/paste)

Fetch and follow this reference (single source of truth):
https://github.com/DK26/strict-path-rs/blob/main/LLM_API_REFERENCE.md

Context7 Style

Fetch and follow this reference (single source of truth):
https://github.com/DK26/strict-path-rs/blob/main/LLM_USER.md

What this crate does

  • Strictly enforces path boundaries: Prevents dangerous paths like ../../../etc/passwd from escaping designated boundaries through compile-time type guarantees
  • Handles the obscure edge cases: Windows 8.3 short names, symlink cycles, NTFS streams, UNC paths, encoding tricks—the stuff you'd never think to test for
  • Compiler-enforced guarantees: StrictPath<Marker> types mathematically prove at compile-time that paths stay strictly within boundaries
  • Enables authorization architectures: When you design markers to require authorization for construction, the compiler mathematically proves that any use of those markers went through authorization first
  • Safe builtin I/O operations: Complete filesystem API (read, write, create_dir, metadata, rename, copy, etc.) that eliminates the need for .interop_path() calls in routine operations
  • Two modes to choose from:
    • StrictPath (default): Detects and rejects escape attempts—perfect for security boundaries where escapes are attacks (archive extraction, file uploads, config loading)
    • VirtualPath (opt-in): Contains and redirects escape attempts—perfect for sandboxes where escapes are expected but must be controlled (malware analysis, multi-tenant isolation)
  • Built on battle-tested foundations: Uses soft-canonicalize which has been validated against 19+ real-world path-related CVEs
  • Easy to use: Drop-in replacement for standard file operations, same return values
  • Works everywhere: Handles platform differences so you don't have to

What this crate is NOT

  • Not just string checking: We actually follow filesystem links and resolve paths properly
  • Not a simple wrapper: Built from the ground up for security, not a thin layer over existing types
  • Not just removing "..": Handles symlinks, Windows short names, and other escape tricks
  • Not a permission system: Works with your existing file permissions, doesn't replace them
  • Not a sandbox: We secure paths at the path level, not at the OS level

Quick start

"If you can read this, you passed the PathBoundary checkpoint."

Policy types first (reusable, explicit)

use strict_path::{PathBoundary, VirtualRoot};

// 1. Define the boundary - paths are contained within ./uploads
let uploads_boundary = PathBoundary::try_new_create("./uploads")?;

// 2. Validate untrusted user input against the boundary
let user_file = uploads_boundary.strict_join("documents/report.pdf")?;

// 3. Safe I/O operations - guaranteed within boundary
user_file.create_parent_dir_all()?;
user_file.write(b"file contents")?;
let contents = user_file.read_to_string()?;

// 4. Escape attempts are detected and rejected
match uploads_boundary.strict_join("../../etc/passwd") {
    Ok(_) => panic!("Escapes should be caught!"),
    Err(e) => println!("Attack blocked: {e}"), // PathEscapesBoundary error
}

// Virtual filesystem for multi-tenant isolation (requires "virtual-path" feature)
let tenant_vroot = VirtualRoot::try_new_create("./tenant_data")?;
let tenant_file = tenant_vroot.virtual_join("../../../sensitive")?;
// Escape attempt is silently clamped - stays within tenant_data
println!("Virtual path: {}", tenant_file.virtualpath_display()); // Shows: "/sensitive"

One-liner sugar (quick prototyping)

use strict_path::{StrictPath, VirtualPath};

// Concise form - boundary created inline
let config_file = StrictPath::with_boundary_create("./config")?
    .strict_join("app.toml")?;
config_file.write(b"settings")?;

let asset = VirtualPath::with_root_create("./public")?
    .virtual_join("images/logo.png")?;
asset.create_parent_dir_all()?;
asset.write(b"logo data")?;

📖 New to strict-path? Start with the Tutorial: Stage 1 - The Basic Promise → to learn the core concepts step-by-step.

The Type-State Police have set up PathBoundary checkpoints
because your LLM is running wild

🚨 One Line of Code Away from Disaster

"One does not simply walk into /etc/passwd."

use strict_path::StrictPath;

let user_input = "../../../etc/passwd";

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

// ✅ This single line makes it mathematically impossible  
let boundary = StrictPath::with_boundary_create("uploads")?;
let result = boundary.strict_join(user_input);
// Returns Err(PathEscapesBoundary) - attack blocked!

Features

  • virtual-path (opt-in): Enables VirtualRoot/VirtualPath and all virtual APIs. By default you get PathBoundary/StrictPath only (with full I/O).

Enable in Cargo.toml:

[dependencies]

strict-path = { version = "...", features = ["virtual-path"] }

Without virtual-path: only PathBoundary + StrictPath are available (I/O remains available); all virtual-only types and methods are removed.

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 — including symlink safety, Windows path quirks, and encoding pitfalls — not just string checks.

Analogy: StrictPath is to paths what a prepared statement is to SQL.

  • The boundary/root you create is like preparing a statement: it encodes the policy (what’s allowed).
  • The untrusted filename or path segment is like a bound parameter: it’s validated/clamped safely via strict_join/virtual_join.
  • The API makes injection attempts inert: hostile inputs can’t escape the boundary, just like SQL parameters can’t change the query.

🛡️ How We Solve The Entire Problem Class

"Symlinks: the ninja assassins of your filesystem."

strict-path isn't just validation—it's a complete solution to path security that handles edge cases you'd never think to check:

  1. 🔧 soft-canonicalize foundation: Heavily tested against 19+ globally known path-related CVEs—the battle-tested work you don't want to reimplement
  2. 🚫 Advanced pattern detection: Catches encoding tricks, Windows 8.3 short names (PROGRA~1), UNC paths, NTFS Alternate Data Streams, and malformed inputs that simple string checks miss
  3. 🔗 Full canonicalization pipeline: Resolves symlinks, junctions, . and .. components, and handles filesystem race conditions—the complex stuff that's easy to get wrong
  4. 📐 Mathematical correctness: Rust's type system provides compile-time proof of path boundaries
  5. 🔐 Authorization architecture: Enable compile-time authorization guarantees through marker types
  6. 👁️ Explicit operations: Method names like strict_join() make security violations visible in code review
  7. 🛡️ Safe builtin I/O operations: Complete filesystem API that reduces the need for .interop_path() calls in routine operations
  8. 🤖 LLM-aware design: Built specifically for untrusted AI-generated paths and modern threat models
  9. ⚡ Dual protection modes: Choose Strict (validate & reject) or Virtual (clamp & contain) based on your use case
  10. 🏗️ Battle-tested architecture: Prototyped and refined across real-world production systems
  11. 🎯 Zero-allocation interop: Seamless integration with existing std::path ecosystems when needed

📖 Read our complete security methodology →
Deep dive into our 7-layer security approach: from CVE research to comprehensive testing

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."

🔍 Comparison with Other Solutions

strict-path vs soft-canonicalize

"High-level safety built on low-level precision."

TL;DR: soft-canonicalize is the foundation; strict-path is the complete security solution built on top.

Aspect soft-canonicalize strict-path
Level Low-level path resolution High-level security API
Purpose Normalize paths for comparison Enforce path boundaries
Philosophy Unopinionated DIY foundation Opinionated easy-to-use safety
Usage Build your own security logic Drop-in secure path handling
Guarantees Resolves paths correctly Prevents directory traversal
Type system Returns PathBuf Returns StrictPath<Marker> with compile-time guarantees

When to use what:

  • Use soft-canonicalize if you need low-level path resolution as a building block for custom path security logic or specialized use cases
  • Use strict-path for comprehensive path security with minimal code - it handles all the security considerations for you

The relationship: strict-path is built on soft-canonicalize, leveraging its battle-tested path resolution (validated against 19+ CVEs) and adding:

  • Boundary enforcement and validation
  • Type-safe marker system for domain separation
  • Complete filesystem I/O API
  • Authorization architecture patterns
  • Ergonomic constructors and integrations

strict-path vs path_absolutize::absolutize_virtually

"Rejection vs containment: different tools for different threats."

Both provide virtual filesystem semantics, but with fundamentally different security models:

Feature path_absolutize::absolutize_virtually strict-path (VirtualPath)
Philosophy Strict validation Permissive sandboxing
Escape handling Error/rejection Clamping to boundary
Symlink resolution None (lexical only) Full resolution (filesystem-based)
Security model "Reject invalid" "Contain within boundary"
Use case Path validation Virtual filesystem
When escapes occur Returns Err Clamps to virtual root

Key insight: These are different security models, not just implementation variations.

Choose path_absolutize::absolutize_virtually when:

  • You want to validate that paths stay within boundaries
  • Escape attempts should be treated as errors
  • You're using lexical-only path resolution (no symlink following)
  • You need strict rejection of invalid paths

Choose strict-path (VirtualPath) when:

  • You need to contain all paths within a boundary regardless of escapes
  • You want filesystem-based canonicalization (handles symlinks, junctions, etc.)
  • You're building multi-tenant systems or sandboxes
  • Escape attempts should be silently contained, not rejected

Choose strict-path (StrictPath) when:

  • You want the rejection behavior but also need full canonicalization
  • You're extracting archives, processing uploads, or loading configs
  • You need both detection AND comprehensive CVE protection

Example difference:

// path_absolutize - rejects escapes
absolutize_virtually("../../../etc/passwd", "/sandbox")?; // Returns Err

// strict-path (VirtualPath) - contains escapes
let vroot = VirtualRoot::try_new("/sandbox")?;
vroot.virtual_join("../../../etc/passwd")?; // Returns Ok("/etc/passwd" within sandbox)

// strict-path (StrictPath) - rejects escapes with full canonicalization
let boundary = PathBoundary::try_new("/sandbox")?;
boundary.strict_join("../../../etc/passwd")?; // Returns Err(PathEscapesBoundary)

Bottom line: Use rejection models when escapes are attacks you want to detect. Use clamping models when you need guaranteed containment regardless of input.

Get Secure in 30 Seconds

[dependencies]

strict-path = "0.1.0-beta.3"

use strict_path::StrictPath;

// 1. Create a boundary (your security perimeter)
//    Use sugar for simple flows; switch to PathBoundary when you need reusable policy
let safe_root = StrictPath::with_boundary("uploads")?;

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

// 3. Use normal file operations - guaranteed secure
safe_path.write(file_data)?;
let info = safe_path.metadata()?; // Inspect filesystem metadata when needed
safe_path.remove_file()?; // Remove when cleanup is required

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

🧬 The Edge Cases You'd Never Think Of

"Security is hard because the edge cases are infinite—until now."

What would you check for when validating a file path? Most developers think: "I'll block ../ and call it a day." But real attackers use techniques you've probably never heard of:

  • Windows 8.3 short names: PROGRA~1Program Files (filesystem aliases that bypass string checks)
  • NTFS Alternate Data Streams: config.txt:hidden:$DATA (secret channels in "normal" files)
  • Unicode normalization: ..∕..∕etc∕passwd (visually identical but different bytes)
  • Symlink time-bombs: Links that resolve differently between validation and use (TOCTOU)
  • Mixed path separators: ../\../etc/passwd (exploiting parser differences)
  • UNC path shenanigans: \\?\C:\Windows\..\..\..\etc\passwd (Windows extended paths)

The reality: You'd need months of research, testing across platforms, and deep filesystem knowledge to handle these correctly.

Our approach: We've already done the research. strict-path is built on soft-canonicalize, which has been battle-tested against 19+ real CVEs. You get comprehensive protection without becoming a path security expert.

🧠 The Secret Weapon: StrictPath<Marker> Types

"Marker types: because your code deserves a secret identity."

The most powerful feature you haven't discovered yet: StrictPath<Marker> doesn't just prevent path attacks—the <Marker> part unlocks secret superpowers that make wrong path usage a compile error.

StrictPath = Promise of path security
<Marker> = Unlocks extra secret powers!

Basic superpower: Prevent cross-domain mix-ups forever. Advanced superpower: Encode authorization requirements into the type system.

Level 1: Basic Domain Separation

use strict_path::{PathBoundary, StrictPath};

struct PublicAssets; // CSS, JS, images
struct UserUploads;  // User documents

let public_assets_dir: PathBoundary<PublicAssets> = PathBoundary::try_new("public")?;
let user_uploads_dir: PathBoundary<UserUploads> = PathBoundary::try_new("uploads")?;

let css_file: StrictPath<PublicAssets> = public_assets_dir.strict_join("style.css")?;
let user_doc: StrictPath<UserUploads> = user_uploads_dir.strict_join("report.pdf")?;

fn serve_public_asset(asset: &StrictPath<PublicAssets>) { /* ... */ }

serve_public_asset(&css_file);    // ✅ Works
// serve_public_asset(&user_doc); // ❌ Compile error: wrong domain!

The power: Mix up user uploads with public assets? Impossible. The compiler catches domain violations.

📖 Tutorial: Stage 3 - Markers → explains marker fundamentals and domain separation patterns.

Level 2: Authorization Architecture

// Marker describes the user's home directory inside a shared filesystem
struct UserHome { _proof: () }

impl UserHome {
    pub fn authenticate_home_access(token: &Token) -> Result<Self, AuthError> {
        verify_token(token)?;  // Real authentication here
        Ok(UserHome { _proof: () })
    }
}

// Functions work with pre-authorized paths
fn read_home_file(path: &StrictPath<UserHome>) -> io::Result<String> {
    // Guaranteed: path is safe AND user passed authentication
    path.read_to_string()
}

The power: Access user home directories without authentication? Impossible. The compiler mathematically proves authorization happened first.

Level 3: Permission Matrix

use strict_path::{PathBoundary, StrictPath};

// Resource types (what) + Permission levels (how)
struct SystemFiles;
struct ReadOnly { _proof: () }
struct AdminPermission { _proof: () }

fn view_system_file(path: &StrictPath<(SystemFiles, ReadOnly)>) -> io::Result<String> {
    path.read_to_string() // Can read, but not modify
}

fn manage_system_file(path: &StrictPath<(SystemFiles, AdminPermission)>) -> io::Result<()> {
    path.write("admin changes") // Full control
}

// Authentication returns the complete tuple
let (system_access, readonly_perm) = authenticate_user(&credentials)?;
let system_files_dir: PathBoundary<SystemFiles> = PathBoundary::try_new("system")?;
let system_file: StrictPath<(SystemFiles, ReadOnly)> = 
    system_files_dir.strict_join("config.txt")?;

view_system_file(&system_file)?;    // ✅ Has ReadOnly permission  
// manage_system_file(&system_file)?; // ❌ Needs AdminPermission!

The power: Create authorization matrices at compile-time. Wrong permission level? Impossible. The type system enforces your security model.

Why This Changes Everything

  • Zero runtime cost: All marker logic erased at compile time
  • Refactoring safety: Change authorization requirements → get compile errors everywhere affected
  • Self-documenting: Function signatures show exactly what permissions are needed
  • Impossible to bypass: No runtime checks to forget or skip

Bottom line: Turn authorization bugs from "runtime disasters" into "won't compile" problems.

📚 Learn Authorization Patterns →: Step-by-step tutorial on encoding authorization in the type system with change_marker(), tuple markers, and capability-based patterns.

Where This Makes Sense

"LLMs: great at generating paths, terrible at keeping secrets."

  • Usefulness for LLM agents: LLMs can produce arbitrary paths; StrictPath/VirtualPath make those suggestions safe by validation (strict) or clamping (virtual) before any I/O.
  • PathBoundary/VirtualRoot: When you want the compiler to enforce that a value is anchored to the initial root/boundary. Keeping the policy type separate from path values prevents helpers from “picking a root” silently. With features enabled, you also get ergonomic, policy‑aware constructors (e.g., dirs, tempfile, app-path).
  • Marker types: Add domain context for the compiler and reviewers (e.g., PublicAssets, UserUploads). They read like documentation and prevent cross‑domain mix‑ups at compile time.

Trade‑offs you can choose explicitly:

  • Zero‑trust, CVE‑aware approach: Prefer canonicalized solutions (this crate) to resolve to absolute, normalized system paths with symlink handling and platform quirks addressed. This defends against entire classes of traversal and aliasing attacks.
  • Lexical approach (performance‑first, limited scope): If you’re absolutely certain there are no symlinks, junctions, mounts, or platform‑specific aliases and your inputs are already normalized, a lexical solution from another crate may be faster. Use this only when the invariants are guaranteed by your environment and tests.

🎯 Decision Guide: When to Use What

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

Source/Input Choose Why Notes
HTTP/CLI args/config/LLM/DB (untrusted segments) StrictPath Detect and reject attacks explicitly Validate with PathBoundary.strict_join(...)
Archive extraction, file uploads StrictPath Detect malicious paths; fail on escape Use error handling to reject compromised files
Malware analysis sandbox, multi-tenant isolation VirtualPath Contain escapes; observe safely Per-user VirtualRoot; use .virtual_join(...)
UI-only path display (multi-user systems) VirtualPath Show clean rooted paths virtualpath_display(); no system leakage
Your own code/hardcoded paths Path/PathBuf You control the value Never for untrusted input
External APIs/webhooks/inter-service messages StrictPath System-facing interop/I/O requires validation Validate on consume before touching FS
(See the full decision matrix in the book)

Critical distinction - Detect vs. Contain:

  • StrictPath (default): Detects escape attempts and returns Err(PathEscapesBoundary). Use when path escapes indicate malicious intent (archive extraction, file uploads, config loading). No feature required.
  • VirtualPath (opt-in): Contains escape attempts by clamping to virtual root. Use when path escapes are expected but must be controlled (malware sandboxes, multi-tenant isolation). Requires virtual-path feature.

Symlink behavior:

  • VirtualPath: Implements true virtual filesystem semantics. When a symlink points to /etc/config, that absolute path is clamped to vroot/etc/config. Perfect for multi-tenant systems where each user's / is actually their virtual root.
  • StrictPath: Uses system filesystem semantics. Symlinks point to their actual system targets. Best for shared system resources and detecting malicious symlinks.

Notes that matter:

  • VirtualPath conceptually extends StrictPath with a virtual "/" view; both support I/O and interop. Choose based on whether escapes are attacks (detect with StrictPath) or expected (contain with VirtualPath).
  • Unified helpers: Prefer dimension-specific signatures. When sharing a helper across both, accept &StrictPath<_> and call with vpath.as_unvirtual() as needed.

At‑a‑glance: API Modes

Feature Path/PathBuf StrictPath VirtualPath
Security None 💥 Validates & rejects ✅ Clamps any input ✅
Join safety Unsafe (can escape) Boundary-checked Boundary-clamped
Symlink targets System paths (can escape) System paths (validated) Clamped to virtual root
Example attack "../../../etc/passwd"System breach "../../../etc/passwd"Error "../../../etc/passwd"/etc/passwd (safe)
Symlink attack link -> /etc/passwdSystem breach link -> /etc/passwdError link -> /etc/passwd/etc/passwd (safe)
Best for Known-safe paths System boundaries User interfaces, multi-tenant storage

📚 Further Reading in the Complete Guide:

🛡️ Core Security Foundation

"StrictPath: the vault door, not just a velvet rope."

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.

Unique capability: By making markers authorization-aware, strict-path becomes the foundation for compile-time authorization architectures - where the compiler mathematically proves that any path with an authorization-requiring marker went through proper authorization during construction.

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.

📚 Visual Guide: Examples by Mode →
Quick visual reference showing when to use StrictPath vs VirtualPath vs Path/PathBuf with side-by-side code examples

📖 Complete Decision Matrix →
Detailed decision matrix with 15+ scenarios, security philosophy, and real-world guidance

🚀 Real-World Examples

"Every example here survived a close encounter with an LLM."

LLM Agent File Manager

use strict_path::PathBoundary;

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

    match request.operation.as_str() {
        "write" => safe_path.write(&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::VirtualPath;

// Encode guarantees in signature: construct a root once; pass untrusted entry names
fn extract_zip(zip_entries: impl IntoIterator<Item=(String, Vec<u8>)>) -> std::io::Result<()> {
    let extract_root = VirtualPath::with_root("./extracted")?;
    for (name, data) in zip_entries {
        // Hostile names like "../../../etc/passwd" get clamped to "/etc/passwd"
        let vpath = extract_root.virtual_join(&name)?; // ✅ Zip slip impossible
        vpath.create_parent_dir_all()?;
        vpath.write(&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()?))
}

// Function signature prevents bypass - no validation needed inside!
async fn serve_file(safe_path: &strict_path::StrictPath<StaticFiles>) -> Response {
    Response::new(safe_path.read().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

"If your attacker has root, strict-path can't save you—but it can make them work for it."

What this protects against (99% of attacks)—including edge cases most developers miss:

  • Basic path traversal: ../../../etc/passwd
  • Symlink escapes: Following links outside boundaries, including directory bombs and cycle detection
  • Archive extraction attacks: Zip slip and similar archive-based traversal attempts
  • Encoding bypass attempts: Unicode normalization attacks, null bytes, and other encoding tricks
  • Windows platform-specific attacks:
    • 8.3 short name aliasing (PROGRA~1Program Files)
    • UNC path manipulation (\\?\C:\ and \\server\share\)
    • NTFS Alternate Data Streams (file.txt:hidden)
    • Drive-relative path forms and junction points
  • Race conditions: TOCTOU (Time-of-Check-Time-of-Use) during path resolution
  • Canonicalization edge cases: Mixed separators, redundant separators, current/parent directory references

The reality: These aren't theoretical attacks—they're real vulnerabilities found in production systems. Instead of researching each CVE and implementing custom defenses, you get comprehensive protection from day one.

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—and handles all the edge cases you'd probably forget to check.

🔐 Advanced: Type-Safe Context Separation

"Type safety: because mixing up user files and web assets is so 2005."

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(web_asset_file: &StrictPath<WebAssets>) -> Response { /* ... */ }
fn process_upload(user_file: &StrictPath<UserFiles>) -> Result<()> { /* ... */ }

// Create context-specific roots
let public_assets_root: VirtualRoot<WebAssets> = VirtualRoot::try_new("public")?;
let user_uploads_root: VirtualRoot<UserFiles> = VirtualRoot::try_new("uploads")?;

let css_file: VirtualPath<WebAssets> = public_assets_root.virtual_join("app.css")?;
let report_file: VirtualPath<UserFiles> = user_uploads_root.virtual_join("report.pdf")?;

// Type system prevents context mixing
serve_asset(css_file.as_unvirtual());         // ✅ Correct context
// serve_asset(report_file.as_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 app_config_dir = PathBoundary::try_new_create(AppPath::new("MyApp").get_app_dir())?;
let safe_config = app_config_dir.strict_join(user_config_name)?; // ✅ Validated
safe_config.write(&settings)?;

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

"Don't be that developer: use the right display method."

DON'T Mix Interop with Display

use strict_path::PathBoundary;
let user_uploads_dir = PathBoundary::try_new("./uploads")?; // user uploads directory boundary

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

// ✅ CORRECT: Use proper display methods
println!("Path: {}", user_uploads_dir.strictpath_display());

// For virtual flows, prefer `VirtualPath` and borrow strict view when needed:
use strict_path::VirtualPath;
let user_uploads_vroot = VirtualPath::with_root("./uploads")?; // user uploads root
let profile_avatar_file = user_uploads_vroot.virtual_join("profile/avatar.png")?; // file by domain role
println!("Virtual: {}", profile_avatar_file.virtualpath_display());
println!("System: {}", profile_avatar_file.as_unvirtual().strictpath_display());

Why this matters:

  • interop_path() is designed solely for unavoidable third-party 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()?))
}

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

Archive Extraction (Zip Slip Prevention)

📘 Complete Archive Extraction Guide → - Full patterns for ZIP/TAR handling, security rationale, and anti-patterns

Cloud Storage API

// User chooses any path - always safe
let user_cloud_root = VirtualPath::with_root(format!("/cloud/user_{id}"))?;
let user_cloud_file = user_cloud_root.virtual_join(&user_requested_path)?; // ✅ Always safe
user_cloud_file.write(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() // ✅ Validated
}

LLM/AI File Operations

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

📚 Documentation & Resources

"If you read the docs, you get +10 security points."

🔌 Integrations

"Integrate like a pro: strict-path plays nice with everyone except attackers."

📄 License

MIT OR Apache-2.0