Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Best Practices & Guidelines

Your complete guide to using strict-path correctly and ergonomically.

This page provides the essential decision matrices, core principles, and quick references for daily use. For deeper dives, we've split detailed content into focused chaptersβ€”each covering a single topic so you can digest one concept at a time.


πŸ“š Focused Chapters (Deep Dives)

For detailed explanations and comprehensive examples, see these focused chapters:

Start here for fundamentals, then jump to focused chapters when you need details.


Why strict-path Exists (TL;DR)

Path security isn't one problemβ€”it's a class of interacting problems. Every "simple" approach (check for ../, canonicalize then check, normalize, allowlist chars, combine checks) creates new attack surface:

  • Encoding bypasses (URL encoding, double encoding, Unicode normalization)
  • Race conditions (TOCTOU with symlinks: CVE-2022-21658)
  • Platform gaps (Windows 8.3 names, UNC paths, ADS: CVE-2019-9855, CVE-2017-17793)
  • Performance costs (repeated filesystem calls)
  • Future CVEs (new attack vectors require updating every check)

strict-path solved this problem class once, correctly, so you don't have to.

β†’ Full analysis with CVE examples β†’


Pick The Right Type (Quick Reference)

30-Second Decision Guide

  • Path/PathBuf (std): When the path comes from a safe source within your control, not external input.
  • StrictPath: When you want to restrict paths to a specific boundary and error if they escape.
  • VirtualPath: When you want to provide path freedom under isolation.

For policy reuse across many joins: Keep a PathBoundary or VirtualRoot and call strict_join(..)/virtual_join(..) repeatedly.

Quick patterns:

  • Detection (90% of cases): StrictPath::with_boundary(..).strict_join(..) β€” detects escapes, rejects attacks
  • Containment (10% of cases): VirtualPath::with_root(..).virtual_join(..) β€” silently clamps escapes, isolates users

Decision Matrix by Source

SourceTypical InputDefault ChoiceNotes
🌐 HTTP/WebURL segments, form fieldsVirtualPath or StrictPathVirtualPath for UI display, StrictPath for system I/O
βš™οΈΒ Config/DBPaths in config/databaseStrictPathStorage β‰  safety; validate on use
πŸ“‚ CLI/External APIsArgs, webhooks, payloadsStrictPathNever trust external input
πŸ€– LLM/AIGenerated paths/filenamesStrictPathLLM output is untrusted by default
πŸ“¦ ArchivesZIP/TAR entry namesStrictPath ONLYDetect malicious paths, reject bad archives
🏒 Multi-tenantPer-user file operationsVirtualPathIsolate users with virtual roots

Security Philosophy: Detect vs. Contain

The fundamental distinction: Are path escapes attacks or expected behavior?

StrictPath β€” Detect & Reject (Default, 90%)

  • Philosophy: "If it tries to escape, I want to know"
  • Returns: Err(PathEscapesBoundary) on escape attempts
  • Use for: Archives, file uploads, config loading, security boundaries
  • Always available (no feature flag)

VirtualPath β€” Contain & Redirect (Opt-in, 10%)

  • Philosophy: "Let it try to escape, but silently contain it"
  • Behavior: Silently clamps escapes within boundary
  • Use for: Multi-tenant systems, malware sandboxes, security research, user isolation
  • Requires: virtual-path feature in Cargo.toml

How They Differ

Attempting ../../../etc/passwd:

  • StrictPath: Err(PathEscapesBoundary) β†’ log, alert, reject
  • VirtualPath: Silently clamped to boundary β†’ contained, not reported

Symlink to /etc/passwd:

  • StrictPath: Follows, validates target β†’ Error if outside boundary
  • VirtualPath: Treats as relative β†’ clamped to vroot/etc/passwd

Critical Rule: Use StrictPath for archives to detect attacks. VirtualPath hides them.

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

β†’ Full comparison with examples β†’


When to Use Policy Types vs. Sugar

Sugar constructors (StrictPath::with_boundary(..), VirtualPath::with_root(..)) are great for simple, one-off operations.

Policy types (PathBoundary, VirtualRoot) matter when you need:

  • Policy reuse (canonicalize once, join many times)
  • Performance (1 canonicalization vs 1000 in loops)
  • Testability (inject test boundaries)
  • Serde integration (contextual deserialization)
  • Clear signatures (encode guarantees in types)

Quick Example:

#![allow(unused)]
fn main() {
use strict_path::PathBoundary;

// ҝŒ SLOW: 1000 canonicalizations
for name in files {
    let boundary = PathBoundary::try_new(base)?;
    boundary.strict_join(name)?;
}

// βœ… FAST: 1 canonicalization
let boundary = PathBoundary::try_new(base)?;
for name in files {
    boundary.strict_join(name)?; // Reuses canonical state
}
}

Rule of thumb: Start with sugar; upgrade to policy types when you need reuse, performance, or testing.

β†’ Full guide with benchmarks, serde patterns, and testing examples β†’


Encode Guarantees In Function Signatures

Helpers that touch the filesystem must encode safety in their signatures:

Two canonical patterns:

  1. Accept validated path when validation already happened: fn save(p: &StrictPath) -> io::Result<()>
  2. Accept boundary + segment when validation happens inside: fn load(b: &PathBoundary, name: &str) -> io::Result<String>

Don't construct boundaries/roots inside helpers β€” boundary choice is policy; make it explicit at call sites.

β†’ Full patterns with examples β†’


Multi-User Isolation (VirtualPath)

VirtualPath (opt-in via virtual-path feature) is for containment scenarios: multi-tenant systems, malware sandboxes, security research.

  • Per-user: Create VirtualRoot per user, call virtual_join(..) for all operations
  • Share helpers: Borrow strict view with vpath.as_unvirtual()
  • Use for: Multi-tenant isolation, observing malicious behavior safely

NOT for: Archive extraction (use StrictPath to detect attacks, not hide them)

β†’ Full multi-tenant example β†’


Interop & Display

Interop (pass to AsRef<Path> APIs): path.interop_path() β€” no allocations

Display:

  • System paths: strictpath_display() on PathBoundary/StrictPath
  • User-facing: virtualpath_display() on VirtualPath

Never: interop_path().to_string_lossy() for display


Directory Discovery vs Validation

Discovery (walking): Use .read_dir(), collect names via entry.file_name()

Validation: Re-join discovered names through strict_join/virtual_join before I/O

#![allow(unused)]
fn main() {
for entry in boundary.read_dir()? {
    let name = entry?.file_name();
    let validated = boundary.strict_join(&name.to_string_lossy())?; // Validate!
    // Now safe to use validated path
}
}

Don't validate constants like "." β€” only validate untrusted segments.


Multi-User Isolation (VirtualPath for Containment)

Note: VirtualPath is opt-in via the virtual-path feature. Use it when you need containment (multi-tenant isolation, sandboxes) rather than detection (security boundaries).

  • Per-user/tenant: for small flows, construct a root via VirtualPath::with_root(..) and join untrusted names with virtual_join(..). For larger flows and reuse, create a VirtualRoot per user and call virtual_join(..).
  • Share strict helpers by borrowing the strict view: vpath.as_unvirtual().
#![allow(unused)]
fn main() {
fn upload(user_root: &VirtualRoot, filename: &str, bytes: &[u8]) -> std::io::Result<()> {
  let vpath = user_root.virtual_join(filename)?;
  vpath.create_parent_dir_all()?;
  vpath.write(bytes)
}

// Sugar-first call site (one-off):
// let vroot = VirtualPath::with_root(format!("./cloud/user_{user_id}"))?;
// let vpath = vroot.virtual_join(filename)?; // same guarantees; keep VirtualRoot for reuse
}

When to use each for archives:

  • StrictPath for production archive extraction β€” detect malicious paths, reject compromised archives, alert users
  • VirtualPath for sandbox/research β€” safely analyze suspicious archives by containing escapes while observing behavior
  • StrictPath for file uploads to shared storage β€” reject attacks at the security boundary
  • StrictPath for config loading β€” fail explicitly on untrusted paths that try to escape

The key: use StrictPath to detect attacks in production; use VirtualPath to contain behavior in research/analysis scenarios.

Interop & Display

  • Interop (pass into AsRef<Path> APIs): path.interop_path() (no allocations).
  • Display:
    • System-facing: strictpath_display() on PathBoundary/StrictPath
    • User-facing: virtualpath_display() on VirtualPath
  • Never use interop_path().to_string_lossy() for display.

Directory Discovery vs Validation

  • Discovery (walking): call boundary.read_dir() (or vroot.read_dir()), collect names via entry.file_name(), then re-join with strict_join/virtual_join to validate before I/O.
  • Validation: join those relatives via boundary.strict_join(..) or vroot.virtual_join(..) before I/O. For small flows without a reusable root, you can construct via StrictPath::with_boundary(..) or VirtualPath::with_root(..) and then join.
  • Don't validate constants like "."; only validate untrusted segments.

Common Operations (Quick Reference)

Always use dimension-specific methods (strict_* / virtualpath_*). Never use std::path methods on leaked paths.

Available operations:

  • Joins: strict_join(..) / virtual_join(..) β€” validate and combine paths
  • Parents: strictpath_parent() / virtualpath_parent() β€” navigate up directory tree
  • Filenames: strictpath_with_file_name(..) / strictpath_with_extension(..) β€” modify names/extensions
  • Rename: strict_rename(..) / virtual_rename(..) β€” move/rename within boundary
  • Deletion: .remove_file(), .remove_dir(), .remove_dir_all() β€” safe deletion
  • Metadata: .metadata(), .exists(), .is_file(), .is_dir() β€” inspect properties
  • Copy: .copy(&dest) β€” duplicate files
  • I/O: .read(), .read_to_string(), .write(), .create_file() β€” file operations

β†’ Complete operations guide with examples β†’

Naming Conventions

Variables reflect domain, not type:

  • βœ… Good: config_dir, uploads_root, archive_src, tenant_vroot
  • ҝŒ Bad: boundary, jail, source_ prefix, _path suffix

Keep names consistent with the directory they represent.


Do / Don't

  • Do: validate once at the boundary, pass types through helpers.
  • Do: use VirtualRoot for per-user isolation; borrow strict view for shared helpers.
  • Do: prefer impl AsRef<Path> in helper params where you forward to validation.
  • Don't: wrap secure types in Path::new/PathBuf::from.
  • Don't: use interop_path().as_ref() or as_unvirtual().interop_path() (use interop_path() directly).
  • Don't: use lossy strings for display or comparisons.

Testing & Doctests

  • Encode guarantees in function signatures
  • Use *_create constructors for temp directories in tests
  • Prefer offline simulations with deterministic inputs
  • Clean up test directories after tests

Learn More

All detailed content has been moved to focused chapters for digestibility:

Core Concepts:

Practical Guides:

Advanced Topics:


Quick Reference Card

Type Selection (30 seconds)

Input SourceDefault ChoiceNotes
HTTP/Web/LLM/ArchivesStrictPathDetect attacks, reject bad input
Multi-tenant isolationVirtualPathContain per-user, clean UI paths
Trusted/hardcodedPath/PathBufOnly validate when mixing untrusted

Sugar vs Policy Types

NeedUse
One-off operationSugar: with_boundary(..) / with_root(..)
Reuse, performance, testPolicy: PathBoundary / VirtualRoot

Core Operations

#![allow(unused)]
fn main() {
// Validate
let file = boundary.strict_join("path")?;

// I/O
file.write(b"data")?;
let content = file.read_to_string()?;

// Metadata
if file.exists() && file.metadata()?.len() > 0 { }

// Rename/Move
let renamed = file.strict_rename("newpath")?;

// Display
println!("System: {}", file.strictpath_display());
println!("User: {}", vpath.virtualpath_display()); // VirtualPath only
}

Do / Don't Checklist

βœ… DO:

  • Validate untrusted segments before I/O
  • Pass &StrictPath / &VirtualPath to encode guarantees
  • Use dimension-specific methods (strict_* / virtualpath_*)
  • Call interop_path() only for AsRef<Path> APIs
  • Name variables by domain (uploads_root, config_dir)

ҝŒ DON'T:

  • Wrap secure types in Path::new() / PathBuf::from()
  • Use std::path methods on leaked paths
  • Use interop_path() for display (use *_display())
  • Construct boundaries inside helpers
  • Validate constants like "." (only untrusted segments)

Anti-Patterns Reference

For detailed anti-patterns and fixes, see: Anti-Patterns Guide β†’


That's it! This page is your quick reference. Dive into the focused chapters when you need details.

For source-level API documentation: API Reference (docs.rs) β†’