# strict-path
[](https://crates.io/crates/strict-path)
[](https://docs.rs/strict-path)
[](https://github.com/DK26/strict-path-rs#license)
[](https://github.com/DK26/strict-path-rs/actions/workflows/ci.yml)
[](https://github.com/DK26/strict-path-rs/actions/workflows/audit.yml)
[](https://github.com/DK26/strict-path-rs)
**Prevent directory traversal with type-safe path restriction and safe symlinks.**
Quick start
```rust
use strict_path::{StrictPath, VirtualPath};
// Strict system path rooted at ./data
let sp = StrictPath::with_boundary("./data")?
.strict_join("users/alice.txt")?;
// Virtual view rooted at ./public (displays as "/...")
let vp = VirtualPath::with_root("./public")?
.virtual_join("assets/logo.png")?;
```
> *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**
```rust
// β This single line can destroy your server
std::fs::write(user_input, data)?; // user_input = "../../../etc/passwd"
// β
This single line makes it mathematically impossible
StrictPath::with_boundary("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`](https://github.com/DK26/soft-canonicalize-rs) 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**
```toml
[dependencies]
strict-path = "0.1.0-alpha.5"
```
```rust
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_bytes(file_data)?;
let info = safe_path.metadata()?; // Inspect filesystem metadata when needed
safe_path.remove_file()?; // Remove when cleanup is required
safe_path.remove_file()?; // Remove when cleanup is required
```
**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**
| **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"*
```rust
use strict_path::VirtualPath;
// Archive extraction - hostile names get clamped, not rejected
let extract_root = VirtualPath::with_root("./extracted")?;
for entry_name in malicious_zip_entries {
let safe_path = extract_root.virtual_join(entry_name)?; // "../../../etc" β "/etc"
safe_path.write_bytes(entry.data())?; // Always safe
}
// User cloud storage - users see friendly paths
let doc = VirtualPath::with_root(format!("users/{user_id}"))?
.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"*
```rust
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"*
```rust
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.**
| **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
```rust
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)
```rust
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)?;
vpath.create_parent_dir_all()?;
vpath.write_bytes(&data)?;
}
Ok(())
}
```
### Web File Server
```rust
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
```rust
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**
| π **HTTP requests** | URL path segments, file names | Display/logging, safe virtual joins | System-facing interop/I/O | Always clamp user paths via `VirtualPath::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`.
```rust
use strict_path::{StrictPath, VirtualPath};
fn process_file<M>(path: &strict_path::StrictPath<M>) -> std::io::Result<Vec<u8>> {
path.read_bytes()
}
// Call with either type
let spath: StrictPath = StrictPath::with_boundary("directory")?.strict_join("file.txt")?;
process_file(&spath)?;
let vpath: VirtualPath = VirtualPath::with_root("directory")?.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:
```rust
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.as_unvirtual()); // Correct context
// serve_asset(doc.as_unvirtual()); // Compile error!
```
**Your IDE and compiler become security guards.**
**App Configuration with `app_path`:**
```rust
// β 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
```rust
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 virtual flows, prefer `VirtualPath` and borrow strict view when needed:
use strict_path::VirtualPath;
let vpath = VirtualPath::with_root("uploads")?.virtual_join("file.txt")?;
println!("Virtual: {}", vpath.virtualpath_display());
println!("System: {}", vpath.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
```rust
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)
```rust
let extract_root = VirtualPath::with_root("./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
```rust
// User chooses any path - always safe
let file_path = VirtualPath::with_root(format!("/cloud/user_{id}"))?
.virtual_join(&user_requested_path)?;
file_path.write_bytes(upload_data)?;
```
### Configuration Files
```rust
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
```rust
// 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**
| **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 |
```rust
// StrictPath - validate and reject
let path = StrictPath::with_boundary("uploads")?.strict_join("file.txt")?; // Error if unsafe
// VirtualPath - clamp any input safely
let vpath = VirtualPath::with_root("userspace")?.virtual_join("any/path/here")?; // Always works
// Both support the same I/O operations
path.write_bytes(data)?;
vpath.read_to_string()?;
```
## π **Documentation & Resources**
- **π [Complete API Reference](https://docs.rs/strict-path)** - Comprehensive API documentation
- **π [User Guide & Examples](https://dk26.github.io/strict-path-rs/)** - In-depth tutorials and patterns
- **π§ [API_REFERENCE.md](API_REFERENCE.md)** - Quick reference for all methods
- **π οΈ [`soft-canonicalize`](https://github.com/DK26/soft-canonicalize-rs)** - The underlying path resolution engine
## π **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