https://github.com/dk26/jailed-path-rs
Prevent directory traversal with type-safe virtual path jails and safe symlinks
https://github.com/dk26/jailed-path-rs
directory-traversal file-security filesystem-security path-traversal-prevention path-validation rust rust-crate security type-safety web-security
Last synced: 6 months ago
JSON representation
Prevent directory traversal with type-safe virtual path jails and safe symlinks
- Host: GitHub
- URL: https://github.com/dk26/jailed-path-rs
- Owner: DK26
- License: other
- Created: 2025-07-16T14:07:55.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2025-08-09T19:57:33.000Z (6 months ago)
- Last Synced: 2025-08-09T21:23:20.061Z (6 months ago)
- Topics: directory-traversal, file-security, filesystem-security, path-traversal-prevention, path-validation, rust, rust-crate, security, type-safety, web-security
- Language: Rust
- Homepage: https://docs.rs/jailed-path
- Size: 434 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE-APACHE
- Codeowners: .github/CODEOWNERS
- Roadmap: ROADMAP.md
Awesome Lists containing this project
README
# jailed-path
[](https://crates.io/crates/jailed-path)
[](https://docs.rs/jailed-path)
[](https://github.com/DK26/jailed-path-rs#license)
[](https://github.com/DK26/jailed-path-rs/actions/workflows/ci.yml)
[](https://github.com/DK26/jailed-path-rs)
**Prevent directory traversal with type-safe virtual path jails and safe symlinks**
> *Putting your paths in jail by the Type-State Police Department*
> *because your LLM can't be trusted with security*
`JailedPath` is a filesystem path **mathematically proven** to stay within directory boundaries. Unlike libraries that hope validation works, we mathematically prove it at compile time using Rust's type system. Two ways to create `JailedPath` instances: `try_jail()` for one-shot path validation, and `Jail::try_new()` + `jail.try_path()` for reusable jail instances. Both guarantee containmentβeven malicious input like `../../../etc/passwd` gets safely clamped.
**Zero Learning Curve**: Two simple functions solve 99% of use cases. **Attack Impossibility**: Not just "hard to bypass" - actually impossible due to API design.
```rust
use jailed_path::{Jail, JailedPath, try_jail};
// β
SECURE - Guaranteed safe by construction
fn serve_file(safe_path: &JailedPath) -> std::io::Result> {
safe_path.read_bytes() // Built-in safe operations
}
# std::fs::create_dir_all("users/alice_workspace/documents")?;
# std::fs::write("users/alice_workspace/documents/report.pdf", b"Alice's report")?;
// Main pattern: Reusable jail for multiple validations (most common)
let user_jail: Jail = Jail::try_new("users/alice_workspace")?;
let safe_path: JailedPath = user_jail.try_path("documents/report.pdf")?;
// Alternative: One-shot validation (for occasional use)
let one_shot_path: JailedPath = try_jail("users/alice_workspace", "documents/report.pdf")?;
// Even attacks are neutralized:
let attack_path = user_jail.try_path("../../../etc/passwd")?;
assert!(attack_path.ends_with("users/alice_workspace")); // Attack contained!
# std::fs::remove_dir_all("users").ok();
# Ok::<(), Box>(())
```
## Key Features: Security-First Design
π **Security First**: API makes unsafe operations impossible, not just difficult
ποΈ **Mathematical Guarantees**: Rust's type system proves security at compile time
π‘οΈ **Zero Attack Surface**: No `Deref` to `Path`, no `AsRef`, validation cannot be bypassed
π **Built-in Safe Operations**: Direct file operations on jailed paths without exposing raw filesystem paths
ποΈ **Virtual Root Display**: Clean user-facing paths that never leak filesystem structure
π― **Multi-Jail Safety**: Marker types prevent cross-jail contamination
π¦ **Minimal Attack Surface**: Only one dependency - our auditable `soft-canonicalize` crate (handles non-existent paths unlike `std::fs::canonicalize`)
π **Type-History Design**: Internal pattern ensures paths carry proof of validation stages
π§ͺ **Comprehensive Testing**: 100%+ test coverage with attack scenario simulation
π **Cross-Platform**: Works on Windows, macOS, and Linux
π€ **LLM-Friendly**: Documentation and APIs designed for both humans and AI systems to understand and use correctly
## The Problem: Every Path Is a Security Risk
```rust
// π¨ DANGEROUS - This code looks innocent but has a critical vulnerability
fn serve_file(path: &str) -> std::io::Result> {
std::fs::read(format!("./public/{path}")) // β Path traversal attack possible!
}
// Attacker sends: "../../../etc/passwd"
// Your server happily serves: ./public/../../../etc/passwd β /etc/passwd π
```
**The brutal truth**: Manual path validation is error-prone and easy to bypass. Even security-conscious developers get it wrong.
## See The Promise In Action: Detailed Examples
```rust
use jailed_path::{try_jail, Jail, JailedPath};
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// THE PROMISE HANDLES ATTACKS - Even escape attempts honor the containment promise:
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
let escape_attempt = "../../../etc/passwd";
let attack_path: JailedPath = try_jail("users/alice_workspace", escape_attempt)?;
// Virtual display shows clamped path - the promise includes hiding real filesystem structure
assert_eq!(attack_path.virtual_display(), "/etc/passwd"); // Clean display, but SAFELY clamped
// β
The promise is verified: this path is actually contained within the jail
assert!(attack_path.ends_with("users/alice_workspace")); // PROOF: Real path is inside the jail!
// Option 2: Reusable jail with try_path()
let alice_home_jail = Jail::try_new("./users/alice_workspace")?;
let vacation_photo_path: JailedPath = alice_home_jail.try_path("photos/vacation.jpg")?;
assert_eq!(vacation_photo_path.virtual_display(), "/photos/vacation.jpg"); // Promise: within alice's space!
let cross_user_attack_path: JailedPath = alice_home_jail.try_path("../bob_workspace/secrets.txt")?;
assert_eq!(cross_user_attack_path.virtual_display(), "/bob_workspace/secrets.txt"); // Clean display
assert!(cross_user_attack_path.ends_with("users/alice_workspace")); // PROOF: Still within alice's jail!
```
**The revolutionary insight**: Every `JailedPath` you hold is a cryptographic-strength promise that has been mathematically verified by Rust's type system. You cannot forge this promiseβthere are no other constructors!
## Understanding the Generic Marker System
You might have noticed something in the examples above. Let's explore the "generic trap" and why it's actually a feature:
```rust
use jailed_path::Jail;
// Simple approach - no type annotation needed
let static_files_jail = Jail::try_new("./static/css")?;
let stylesheet_path = static_files_jail.try_path("bootstrap.css")?; // Type: JailedPath<()>
// Or be explicit with the "turbofish" syntax
let static_files_jail: Jail<()> = Jail::try_new("./static/css")?;
let stylesheet_path: JailedPath<()> = static_files_jail.try_path("bootstrap.css")?;
```
**"Why the generic `<()>` parameter?"** - This is Rust's way of saying "no special marker." But the real power comes when you DO use markers...
## The Power of Multiple Jails: Promises with Specific Identities
Real applications have multiple directories. Here's where the promise system becomes even more powerful by adding **identity** to the containment promise:
```rust
use jailed_path::{Jail, JailedPath};
// Define semantic markers for different promise types
struct WebAssets;
struct UserUploads;
// Create type-safe path jails that make specific promises
let cdn_assets_jail: Jail = Jail::try_new("./cdn/assets")?;
let user_uploads_jail: Jail = Jail::try_new("./uploads")?;
// Get paths with specific promises
let css_bundle_path: JailedPath = cdn_assets_jail.try_path("app.bundle.css")?;
// β Promise: "I am contained within ./cdn/assets AND I am a WebAssets path"
let profile_pic_path: JailedPath = user_uploads_jail.try_path("avatars/user123.png")?;
// β Promise: "I am contained within ./uploads AND I am a UserUploads path"
// Functions can require specific promise types
fn serve_cdn_asset(asset: &JailedPath) -> std::io::Result> {
asset.read_bytes() // β
This function ONLY accepts the WebAssets promise
}
// The type system enforces promise contracts
serve_cdn_asset(&css_bundle_path)?; // β
Correct promise type
// serve_cdn_asset(&profile_pic_path)?; // β Compile error! Wrong promise type!
```
**The magic**: The compiler mathematically guarantees that different promise types cannot be mixed up. A `JailedPath` promises both containment AND identityβyou can never accidentally serve a user upload as a CDN asset.
## Even Single Jails Benefit from Semantic Markers
```rust
use jailed_path::{Jail, JailedPath};
struct DocumentStorage;
let docs_jail: Jail = Jail::try_new("./company_docs")?;
fn access_document(file: &JailedPath) -> std::io::Result> {
// The type signature makes it crystal clear what this function expects
file.read_bytes() // β
Safe built-in operation
}
```
The marker adds semantic meaning and prevents accidental misuse.
## Safe File Operations: Why Direct Path Access Can Be Dangerous
Here's a critical security insight: even with a `JailedPath`, getting a raw `&Path` can be risky if misused:
```rust
use jailed_path::Jail;
use std::path::Path;
let customer_data_jail = Jail::try_new("./customer_data")?;
let invoice_path = customer_data_jail.try_path("invoices/2024/invoice-001.pdf")?;
// π¨ DANGEROUS - A raw &Path can be misused!
let raw_path: &Path = invoice_path.as_ref();
let dangerous = raw_path.join("../../../etc/passwd"); // Oops! Escaped the jail!
```
**The solution**: Use our built-in safe operations instead.
## Built-in Safe File Operations
```rust
use jailed_path::Jail;
let customer_uploads_jail = Jail::try_new("./customer_uploads")?;
let contract_path = customer_uploads_jail.try_path("contracts/acme-corp-2024.pdf")?;
// β
SAFE - All operations stay within the jail automatically
contract_path.write_string("Contract updated with new terms")?;
let content = contract_path.read_to_string()?;
assert_eq!(content, "Contract updated with new terms");
// Write operations - always safe
contract_path.write_bytes(b"PDF binary data")?;
let data = contract_path.read_bytes()?;
assert_eq!(data, b"PDF binary data");
// Directory operations - always safe
let client_folder_path = customer_uploads_jail.try_path("new_client_folder")?;
client_folder_path.create_dir_all()?;
assert!(client_folder_path.exists());
// Metadata operations - always safe
contract_path.write_string("Updated contract content")?;
let metadata = contract_path.metadata()?;
assert!(metadata.len() > 0);
```
**No raw path access needed!** All operations are mathematically guaranteed to stay within the jail.
## Virtual Root Display: Clean User-Facing Paths
```rust
use jailed_path::Jail;
let saas_tenant_jail = Jail::try_new("./tenant_data/company_xyz")?;
let report_path = saas_tenant_jail.try_path("reports/quarterly/2024-q1.xlsx")?;
// User sees clean, intuitive paths - never internal filesystem details
assert_eq!(format!("{report_path}"), "/reports/quarterly/2024-q1.xlsx");
// The real path is hidden (and you shouldn't need it anyway!)
assert_eq!(report_path.to_string_lossy(), "./tenant_data/company_xyz/reports/quarterly/2024-q1.xlsx");
```
This prevents leaking internal filesystem structure in logs, error messages, or user interfaces.
## Mathematical Security: Our Type-State Design
This crate uses a sophisticated "Type-History" design pattern internally. Every path carries mathematical proof of what validation stages it has passed through:
```rust
// Internal type-state progression (you don't see this, but it's happening):
// Raw β Clamped β JoinedJail β Canonicalized β BoundaryChecked β JailedPath
```
Our comprehensive test coverage (100%+) and LLM-friendly documentation ensure that every security property is verified mathematically, not just hoped for.
## Complete Attack Immunity Demonstration
```rust
use jailed_path::Jail;
let web_server_jail: Jail = Jail::try_new("./www/htdocs")?;
// β
Normal paths work as expected - legitimate web requests
let homepage_path = web_server_jail.try_path("index.html")?;
assert_eq!(homepage_path.to_string_lossy(), "./www/htdocs/index.html");
assert_eq!(format!("{homepage_path}"), "/index.html");
let stylesheet_path = web_server_jail.try_path("css/main.css")?;
assert_eq!(stylesheet_path.to_string_lossy(), "./www/htdocs/css/main.css");
assert_eq!(stylesheet_path.virtual_display(), "/css/main.css");
// π‘οΈ ATTACK ATTEMPTS ARE MATHEMATICALLY IMPOSSIBLE TO SUCCEED
let shadow_attack_path = web_server_jail.try_path("/etc/shadow")?;
assert_eq!(shadow_attack_path.to_string_lossy(), "./www/htdocs/etc/shadow"); // Harmless!
assert_eq!(shadow_attack_path.virtual_display(), "/etc/shadow"); // In jail
let config_attack_path = web_server_jail.try_path("../config.ini")?;
assert_eq!(config_attack_path.to_string_lossy(), "./www/htdocs"); // Jail root
assert_eq!(config_attack_path.virtual_display(), "/");
let passwd_attack_path = web_server_jail.try_path("../../../etc/passwd")?;
assert_eq!(passwd_attack_path.to_string_lossy(), "./www/htdocs"); // Jail root
assert_eq!(passwd_attack_path.virtual_display(), "/");
// π The attacker CANNOT access the real /etc/passwd - it's mathematically impossible!
assert!(config_attack_path.ends_with("htdocs")); // PROOF: Clamped to jail root
assert!(passwd_attack_path.ends_with("htdocs")); // PROOF: Clamped to jail root
```
## Advanced: Real-World Integration Examples
### Axum Web Server with Multi-Tenant File Serving
```rust
use axum::{extract::Path, response::Response, http::StatusCode};
use jailed_path::Jail;
struct StaticAssets;
struct UserContent;
// Set up path jails for different content types
let static_jail = Jail::::try_new("./public")?;
let content_jail = Jail::::try_new("./user_content")?;
// Axum route handlers with compile-time path safety
async fn serve_static(Path(file_path): Path) -> Result {
let safe_path = static_jail.try_path(&file_path)
.map_err(|_| StatusCode::NOT_FOUND)?;
if !safe_path.exists() {
return Err(StatusCode::NOT_FOUND);
}
let content = safe_path.read_bytes()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Response::new(content.into()))
}
async fn serve_user_content(Path((tenant_id, file_path)): Path<(String, String)>) -> Result {
let tenant_path = format!("{tenant_id}/{file_path}");
let safe_path = content_jail.try_path(&tenant_path)
.map_err(|_| StatusCode::FORBIDDEN)?; // Auto-blocks traversal attacks
// Rest of handler logic...
Ok(Response::new("content".into()))
}
```
Ok(Response::new(content.into()))
}
async fn serve_user_content(Path((tenant_id, file_path)): Path<(String, String)>) -> Result {
let tenant_path = format!("{tenant_id}/{file_path}");
let safe_path = content_jail.try_path(&tenant_path)
.map_err(|_| StatusCode::FORBIDDEN)?; // Auto-blocks traversal attacks
// Rest of handler logic...
Ok(Response::new("content".into()))
}
```
### Cloud Storage Sync Service
```rust
use jailed_path::Jail;
struct LocalCache;
struct RemoteSync;
// Automation service that syncs cloud storage locally
let cache_jail = Jail::::try_new("./cache/downloads")?;
let sync_jail = Jail::::try_new("./sync_staging")?;
async fn download_cloud_file(cloud_path: &str, local_name: &str) -> Result<(), Box> {
// Ensure downloaded files stay in designated cache area
let local_path = cache_jail.try_path(local_name)?;
// Download from cloud service (S3, GCS, etc.)
let cloud_data = fetch_from_cloud(cloud_path).await?;
// Safe write - guaranteed to stay in cache jail
local_path.write_bytes(&cloud_data)?;
println!("Downloaded to: {}", local_path.display()); // Clean path display
Ok(())
}
async fn sync_to_staging(cached_file: &str) -> Result<(), Box> {
let cache_path = cache_jail.try_path(cached_file)?;
let staging_path = sync_jail.try_path(cached_file)?;
// Move between jails safely
let data = cache_path.read_bytes()?;
staging_path.write_bytes(&data)?;
Ok(())
}
```
### Resource Bundling Tool
```rust
use jailed_path::Jail;
struct SourceAssets;
struct BuildOutput;
// Build tool that processes resources from multiple sources
let source_jail = Jail::::try_new("./src/assets")?;
let build_jail = Jail::::try_new("./dist")?;
fn bundle_css_files(css_files: &[&str]) -> Result<(), Box> {
let mut combined_css = String::new();
for css_file in css_files {
// Safe access to source files - no traversal possible
let source_path = source_jail.try_path(css_file)?;
let css_content = source_path.read_to_string()?;
combined_css.push_str(&css_content);
combined_css.push('\n');
}
// Safe output to build directory
let bundle_path = build_jail.try_path("bundle.css")?;
bundle_path.write_string(&combined_css)?;
println!("CSS bundle created: {}", bundle_path.display());
Ok(())
}
fn process_image_assets(image_dir: &str) -> Result<(), Box> {
let source_dir = source_jail.try_path(image_dir)?;
// Process all images in the source directory
for entry in std::fs::read_dir(source_dir.as_ref())? {
let entry = entry?;
let filename = entry.file_name().to_string_lossy().to_string();
if filename.ends_with(".png") || filename.ends_with(".jpg") {
let source_img = source_jail.try_path(&format!("{image_dir}/{filename}"))?;
let output_img = build_jail.try_path(&format!("images/{filename}"))?;
// Safe image processing - both paths are jailed
let img_data = source_img.read_bytes()?;
// ... image optimization logic ...
output_img.write_bytes(&img_data)?;
}
}
Ok(())
}
```
## For One-Shot Validation: Banking Application
Sometimes you need quick path validation without the overhead of creating a validator:
```rust
use jailed_path::try_jail;
// Banking application handling customer statements
fn generate_customer_statement(customer_id: &str, year: &str) -> Result> {
// Quick validation: keep customer statements within their secure directory
let statement_path = try_jail("./bank_statements", format!("customer_{}/statements/{}.pdf", customer_id, year))?;
if !statement_path.exists() {
return Err("Statement not found".into());
}
Ok(format!("Statement available at: {}", statement_path.display()))
}
// Example usage - secure by design
match generate_customer_statement("12345", "2023") {
Ok(location) => println!("{}", location),
Err(e) => println!("Access denied: {}", e), // Handles traversal attacks automatically
}
// What happens with attacks:
// generate_customer_statement("../../../etc", "passwd") -> Error: path escapes jail
// generate_customer_statement("12345", "../other_customer") -> Error: path escapes jail
```
### With External Crates (Portable Paths)
```rust
use app_path::app_path;
use jailed_path::Jail;
struct ConfigFiles;
struct DataFiles;
// Portable paths relative to your executable
let config: Jail = Jail::try_new(app_path!("config"))?;
let data: Jail = Jail::try_new(app_path!("data"))?;
// Type-safe, attack-proof file access
let settings_path = config.try_path("app.toml")?;
let database_path = data.try_path("users.db")?;
```
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
jailed-path = "0.0.4"
```
## License
Licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
- MIT License ([LICENSE-MIT](LICENSE-MIT))
at your option.