Skip to content

Commit 6c8463e

Browse files
authored
Release v0.5.5: Fix .. traversal bypass and expand test coverage (#54)
* CI: removed redundant cargo audit in JSON format * docs: add exotic filesystem support details to README and lib.rs * docs: update README and lib.rs with feature comparison and test count improvements * test: add tests for graceful fallback on exotic filesystem failures * test: add comprehensive security coverage and cross-platform tests Add three new security test files to close identified gaps in vulnerability coverage: 1. tests/ads_cross_platform_security.rs (256 lines, 11 tests) - Populate previously empty file with cross-platform ADS security tests - Windows: ADS traversal rejection, device name in ADS, non-final colon, unicode manipulation - Unix: colon-as-literal-filename semantics, symlink handling with colons - Covers Windows-specific alternate data stream attack vectors 2. tests/security_coverage_gaps.rs (1008 lines, 22+ tests) - New file covering previously untested vulnerability classes: * Unpaired UTF-16 surrogates (Windows): 7 tests on invalid Unicode handling * Permission-change TOCTOU race conditions (Unix): 2 tests on symlink races * Special file types (Unix): 10 tests covering FIFO, sockets, block/char devices * Invalid UTF-8 paths (Unix): 5 tests on encoding edge cases * Junction discrimination (Windows): 4 tests on junction vs directory behavior * Anchored canonicalize edge cases: 6 tests on pseudo-root behavior * Component length attacks: 4 tests on path limits - All clippy warnings resolved (single_match conversions, needless_borrow fixes) - All format checks pass (blank line correction) 3. tests/macos_security.rs (1059 lines, ~50 tests) - New file with macOS-specific security tests (all gated with cfg gate) - NFD/NFC normalization attacks - Case-insensitive filesystem traversal - Symlink and mount point attacks - Resource fork and extended attribute handling - Ready for execution on macOS CI runner Test Results: - All 176+ unit tests passing on Windows - All 22 new security_coverage_gaps tests passing - All 5 Windows ADS tests in ads_cross_platform_security passing - All clippy checks pass with -D warnings - All format checks pass - MSRV 1.70 compatible with no new dependencies Addresses security audit request: comprehensive vulnerability testing across Windows, Unix/Linux, and macOS platforms with zero-regression parity checks. * refactor: simplify match statements in macOS security tests * fix: skip normalized-path fast-path when `..` is present (#53) Lexical normalization in Stage 3 collapsed `symlink/..` without following the symlink, causing `soft_canonicalize` to resolve to a wrong existing path. Skip the normalized fast-path when the absolute path contains `..` components so the slow path can resolve symlinks before applying parent-dir traversal. Add regression tests for the non-existing and existing suffix cases. Closes #53 * refactor: split large test files by platform/concern and extract anchored module Split monolithic test files into focused, single-concern modules: - src/lib.rs: extract `anchored_canonicalize` into src/anchored.rs - src/tests: split CVE, exotic, and platform tests by OS target - tests/: split blackbox, feature-combination, std-compat, security, macos, and Windows 8.3 tests into dedicated files Also includes minor code quality improvements: - Simplify `is_proc_magic_link` with slice pattern matching - Remove dead `DeviceNS` payload in normalize.rs - Avoid intermediate String allocation for drive letters - Use safer indexing in windows.rs ADS validation - Consolidate redundant cfg-gated symlink clamping branches - Rewrite AGENTS.md for clarity and remove corrupted duplicate sections * fix: gate Windows-only test imports behind #[cfg(windows)] Imports used exclusively in #[cfg(windows)] test functions were declared at the top level, causing unused-import errors on Linux CI with `clippy -D warnings`. Move the imports into the cfg-gated modules or add #[cfg(windows)] to the use statements. Files fixed: - tests/security_utf16_surrogates.rs - tests/security_junction_discrimination.rs - src/tests/exotic_windows.rs - src/tests/platform_windows.rs * refactor: remove unused imports from macOS-specific test files * fix(tests): platform-conditional assertions for issue #53 regression test Windows resolves `symlink\..` lexically (before following the symlink), while Unix follows the symlink first then resolves `..`. This means `link\..\a` on Windows reaches the existing decoy `{tmp}\a` and std::fs::canonicalize succeeds — matching that is required by the golden rule. Split the non-existing-suffix test assertion into #[cfg(unix)] and #[cfg(windows)] branches: - Unix: assert symlink-following result (nested/a) - Windows: assert parity with std::fs::canonicalize (+ dunce guard) * chore: update version to 0.5.5 and enhance changelog with new features and tests
1 parent ee5e818 commit 6c8463e

42 files changed

Lines changed: 6996 additions & 3697 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/audit.yml

Lines changed: 32 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,47 @@ name: Security Audit
33
on:
44
# Run on pushes to main/dev branches
55
push:
6-
branches: [ main, dev ]
7-
6+
branches: [main, dev]
7+
88
# Run on pull requests to main
99
pull_request:
10-
branches: [ main ]
11-
10+
branches: [main]
11+
1212
# Schedule to run daily at 2 AM UTC to catch new advisories
1313
schedule:
14-
- cron: '0 2 * * *'
15-
14+
- cron: "0 2 * * *"
15+
1616
# Allow manual triggering
1717
workflow_dispatch:
1818

1919
jobs:
2020
security_audit:
2121
name: Security Audit
2222
runs-on: ubuntu-latest
23-
23+
2424
steps:
25-
- name: Checkout code
26-
uses: actions/checkout@v4
27-
with:
28-
# Fetch full history for accurate dependency analysis
29-
fetch-depth: 0
30-
31-
- name: Install Rust toolchain
32-
uses: dtolnay/rust-toolchain@stable
33-
34-
- name: Cache cargo registry
35-
uses: actions/cache@v4
36-
with:
37-
path: |
38-
~/.cargo/registry/index/
39-
~/.cargo/registry/cache/
40-
~/.cargo/git/db/
41-
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
42-
restore-keys: |
43-
${{ runner.os }}-cargo-
44-
45-
- name: Install cargo-audit
46-
run: cargo install cargo-audit --locked
47-
48-
- name: Run security audit
49-
run: cargo audit
50-
51-
- name: Run security audit with JSON output
52-
run: cargo audit --format json --output audit-results.json
53-
continue-on-error: true
54-
55-
- name: Upload audit results as artifact
56-
uses: actions/upload-artifact@v4
57-
if: always()
58-
with:
59-
name: security-audit-results
60-
path: audit-results.json
61-
retention-days: 30
25+
- name: Checkout code
26+
uses: actions/checkout@v4
27+
with:
28+
# Fetch full history for accurate dependency analysis
29+
fetch-depth: 0
30+
31+
- name: Install Rust toolchain
32+
uses: dtolnay/rust-toolchain@stable
33+
34+
- name: Cache cargo registry
35+
uses: actions/cache@v4
36+
with:
37+
path: |
38+
~/.cargo/registry/index/
39+
~/.cargo/registry/cache/
40+
~/.cargo/git/db/
41+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
42+
restore-keys: |
43+
${{ runner.os }}-cargo-
44+
45+
- name: Install cargo-audit
46+
run: cargo install cargo-audit --locked
47+
48+
- name: Run security audit
49+
run: cargo audit

AGENTS.md

Lines changed: 168 additions & 33 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.5.5] - 2026-04-04
9+
10+
### Fixed
11+
12+
- **Fixed `..` traversal bypass when normalized path differs from original** ([#53](https://github.com/DK26/soft-canonicalize-rs/issues/53)): The normalized-path fast-path (`fs::canonicalize` on the normalized form) was incorrectly succeeding for paths containing `..` components that pointed to existing locations after normalization. This caused `soft_canonicalize` to return a fully-canonicalized result that silently resolved `..` through symlinks instead of applying lexical `..` resolution per the crate's symlink-first semantics. The fast-path is now skipped when `..` is present in the normalized path.
13+
14+
### Changed
15+
16+
- **Refactored source layout**: Extracted `anchored_canonicalize` into dedicated `src/anchored.rs` module (compiled only with `--features anchored`). Split large test files by platform and concern for better maintainability and RAG retrieval.
17+
- **Improved documentation**: Updated README.md and lib.rs with feature comparison tables, exotic filesystem support details, and refreshed test counts.
18+
- **CI**: Removed redundant `cargo audit` JSON-format step from audit workflow.
19+
20+
### Added
21+
22+
- **Regression test for issue #53** (`tests/issue_53_symlink_dotdot_lexical_collapse.rs`): Verifies that `..` traversal respects symlink-first lexical semantics and does not silently resolve through symlinks.
23+
- **Comprehensive security test coverage**: New test files for cross-platform ADS security, anchored edge cases, component length limits, invalid UTF-8 handling, junction discrimination, permission TOCTOU, rename TOCTOU, special files, UTF-16 surrogates, and Windows 8.3 components.
24+
- **Exotic filesystem fallback tests** (`tests/exotic_filesystem_fallback.rs`): Coverage for graceful behavior on exotic filesystem failures.
25+
- **Cross-platform path tests**: macOS NFD normalization, private symlinks, resource forks/firmlinks, and volumes anchored edge cases.
26+
- **Expanded feature combination tests**: Split into core and platform-specific test files.
27+
- **Compatibility test suites**: Dedicated test files for `..` traversal, existing paths, and symlink compatibility with `std::fs::canonicalize`.
28+
829
## [0.5.4] - 2025-01-19
930

1031
### Changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "soft-canonicalize"
3-
version = "0.5.4"
3+
version = "0.5.5"
44
edition = "2021"
55
authors = ["David Krasnitsky <dikaveman@gmail.com>"]
66
description = "Path canonicalization that works with non-existing paths."
@@ -35,6 +35,7 @@ tempfile = "=3.21" # Do not update
3535
# We cannot use `junction` v1.4.1 crate because that would require
3636
# us to bump our MSRV to Rust 1.71, and we only use this crate for testing.
3737
# So, we stick to the workaround `junction-verbatim`
38+
# Do not bump to `1.3.1` or we will get deprecation warnings about `junction-verbatim` crate.
3839
junction-verbatim = "=1.3.0"
3940

4041
[features]

README.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Rust implementation inspired by Python 3.6+ `pathlib.Path.resolve(strict=False)`
2020
**🔒 Robust** - 500+ comprehensive tests including symlink cycle protection, malicious stream validation, and edge case handling
2121
**🛡️ Safe traversal** - Proper `..` and symlink resolution with cycle detection
2222
**🌍 Cross-platform** - Windows, macOS, Linux with comprehensive UNC/symlink handling
23+
**💾 Exotic filesystem support** - Works on RAM disks, network drives, Docker volumes ([rust-lang/rust#45067](https://github.com/rust-lang/rust/issues/45067), [#48249](https://github.com/rust-lang/rust/issues/48249))
2324
**🔧 Zero dependencies** - Optional features may add minimal dependencies
2425

2526
## Lexical vs. Filesystem-Based Resolution
@@ -216,15 +217,16 @@ proc-canonicalize = "0.0"
216217

217218
### Feature Comparison
218219

219-
| Feature | `soft_canonicalize` | `proc_canonicalize` | `std::fs::canonicalize` | `std::path::absolute` | `dunce::canonicalize` |
220-
| -------------------------------- | ----------------------------- | ------------------- | ----------------------- | --------------------- | --------------------- |
221-
| Resolution type | Filesystem-based | Filesystem-based | Filesystem-based | Lexical | Filesystem-based |
222-
| Works with non-existing paths ||||||
223-
| Resolves symlinks ||||||
224-
| Preserves Linux namespaces | ✅ (default) ||| N/A ||
225-
| Simplified Windows paths | ✅ (opt-in `dunce` feature) | ✅ (opt-in) | ❌ (UNC) | ❌ (varies) ||
226-
| Virtual/bounded canonicalization | ✅ (opt-in `anchored` feature) |||||
227-
| Zero dependencies | ✅ (default) |||||
220+
| Feature | `soft_canonicalize` | `normpath` | `proc_canonicalize` | `std::fs::canonicalize` | `std::path::absolute` | `dunce::canonicalize` |
221+
| -------------------------------- | ----------------------------- | ---------- | ------------------- | ----------------------- | --------------------- | --------------------- |
222+
| Resolution type | Filesystem-based | Lexical | Filesystem-based | Filesystem-based | Lexical | Filesystem-based |
223+
| Works with non-existing paths |||||||
224+
| Resolves symlinks |||||||
225+
| RAM disk / network drive support | ✅ (graceful fallback) | ✅ (no I/O) |||||
226+
| Preserves Linux namespaces | ✅ (default) | N/A ||| N/A ||
227+
| Simplified Windows paths | ✅ (opt-in `dunce` feature) | ✅ (opt-in) | ✅ (opt-in) | ❌ (UNC) | ❌ (varies) ||
228+
| Virtual/bounded canonicalization | ✅ (opt-in `anchored` feature) ||||||
229+
| Zero dependencies | ✅ (default) ||||||
228230

229231
### When to Use Each
230232

@@ -235,11 +237,11 @@ proc-canonicalize = "0.0"
235237
- ✅ You need simplified Windows paths for legacy apps (with `dunce` feature)
236238

237239
**Choose alternatives when:**
240+
- **`normpath::normalize`** - Maximum performance needed, you can guarantee no symlinks exist, and you want pure lexical normalization (no I/O). Note: lacks symlink resolution, so not suitable when security against symlink-based attacks is required
238241
- **`proc_canonicalize::canonicalize`** - All paths exist and you need correct Linux namespace handling (recommended over `std::fs::canonicalize`)
239242
- **`std::fs::canonicalize`** - All paths exist; only when you specifically need the legacy behavior that resolves `/proc/PID/root` to `/`
240243
- **`std::path::absolute`** - You only need absolute paths without symlink resolution (lexical, fast)
241244
- **`dunce::canonicalize`** - Windows-only, all paths exist, just need UNC simplification
242-
- **`normpath::normalize`** - Lexical normalization only, no filesystem I/O (fast but doesn't resolve symlinks)
243245
- **`path_absolutize`** - Absolute path resolution without symlink following, with CWD caching optimizations
244246

245247
## Related Projects

0 commit comments

Comments
 (0)