# Security Policy
This document is the threat model and security contract for `path_jail`. It
exists because every security library needs one: callers can only use the
library correctly if they know what it defends against and what it doesn't.
If you find a vulnerability, see [Reporting a vulnerability](#reporting-a-vulnerability) below.
---
## Attacker model
`path_jail` is designed to defend against an attacker who supplies path
strings to your application — for example:
- A web client uploading a file with a chosen filename
- A user-controlled config value naming a path inside a sandbox directory
- A workflow step naming a file inside a CI working directory
The attacker can supply:
- Arbitrary bytes in the path (including `..`, leading `/`, null bytes, magic
link prefixes like `/proc/self/fd/N`)
- A pre-existing symlink inside the jail that points outside the jail
- A pre-existing hard link inside the jail that points to sensitive content
- Concurrent filesystem activity attempting to swap paths between validation
and open (TOCTOU)
The attacker is assumed to **not** have:
- Privileges to mount filesystems, run as root inside the jail's filesystem,
call `ptrace`, or otherwise escape the OS sandbox the application runs in
- The ability to modify the running process's memory
- A working kernel exploit
If your attacker can do any of the above, no userspace library can help —
you need a process-level sandbox (`seccomp`, `landlock`, containers, VMs).
---
## What each API defends against
`path_jail` ships three API layers with different security/ergonomics
tradeoffs. Pick the strongest one your environment supports.
| Path traversal via `..` | ✅ | ✅ | ✅ |
| Absolute path injection (`/etc/passwd`) | ✅ | ✅ | ✅ |
| Null-byte injection | ✅ | ✅ | ✅ |
| Symlink target outside the jail | ✅ | ✅ | ✅ |
| Broken symlinks (cannot verify target) | ✅ | ✅ | ✅ |
| Symlink swap on final component (TOCTOU) | ❌ | ✅ | ✅ |
| Symlink swap on intermediate directories | ❌ | ❌ | ✅ |
| Concurrent rename of jail root mid-operation | ❌ | ❌ | ✅¹ |
| Magic links (`/proc/self/fd`, `/proc/self/root`) | ❌ | ❌ | ✅ |
| Hard link to sensitive content (detect) | ❌² | ❌² | ✅³ |
| Bind-mount escape (opt-in) | ❌ | ❌ | ✅⁴ |
| Atomic open with kernel-enforced containment | ❌ | ❌ | ✅ |
| Signed attestation of the open event | ❌ | ❌ | ✅⁵ |
Footnotes:
1. `guard::FdJail` pins an `O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC` fd to the
jail root at construction time. Subsequent renames or replacements of the
root *path* do not affect the jail — all operations remain scoped to the
original directory inode.
2. Hard links are not detectable in user space before open. The path-based
APIs do not stat the opened file and so cannot surface `nlink`.
3. `guard::JailFile::has_hard_links()` exposes `nlink > 1` from the post-open
`fstat`. **Policy is the caller's responsibility** — a content-addressed
store may legitimately use hard links. If your policy rejects hard links,
check `has_hard_links()` **before** reading or writing.
4. Opt in with `OpenOptions::no_xdev(true)` (maps to `RESOLVE_NO_XDEV`).
Off by default to preserve directory-tree containment semantics, which
are what most callers want.
5. Opt in by implementing the `Signer` trait. `path_jail` ships no crypto;
bring your own (`ed25519-dalek`, `ring`, HSM client, KMS, etc.).
---
## Out of scope
These threats are documented as **not defended** by any API:
- **Privileged local attackers.** A process with root or `CAP_SYS_ADMIN` on
the host can mount, bind-mount, or `ptrace` around any user-space check.
- **Kernel and filesystem exploits.** A kernel-level bug, a FUSE filesystem
misbehaving, or an `openat2` semantic regression in a specific kernel
version are outside our control. We pin to documented kernel ABI.
- **Side-channel attacks.** Timing, cache, filesystem-metadata leaks.
- **Directory iteration (`read_dir`) and recursive walks.** Iterating jail
contents has its own TOCTOU surface (rename-during-walk) that we do not
currently address. If you walk a directory tree, treat it as untrusted
input on every iteration.
- **Windows.** No Windows-specific protections are implemented. `Jail` and
`secure-open` compile on Windows but provide no defenses beyond the
cross-platform path-string checks; `guard` is Linux-only.
- **Unicode normalization.** Paths are accepted byte-for-byte. We do not
normalize NFC/NFD on macOS or fold case on Windows/macOS. If your storage
layer is case-insensitive, treat `Report.PDF` and `report.pdf` as
potentially the same file.
- **Resource exhaustion.** Very long paths, deep symlink chains, etc. are
rejected by the kernel (`ENAMETOOLONG`, `ELOOP`) but `path_jail` does not
impose its own limits.
---
## Choosing the right API
```text
┌──────────────────────────────────┐
You only need a validated │ Use `Jail::join` / `join_typed`. │
path (e.g., for logging) → │ Cheap, portable. │
└──────────────────────────────────┘
┌──────────────────────────────────┐
You open the file in │ Use `secure-open`. │
process, on Unix, and need │ Protects final-component swaps. │
final-component TOCTOU → └──────────────────────────────────┘
┌──────────────────────────────────┐
Security-critical opens on │ Use `guard` (Linux 5.6+). │
Linux, attestation needed, │ Kernel-enforced; signable. │
or hostile multi-tenant → └──────────────────────────────────┘
```
`guard` is the strongest. Use it on Linux where you can.
---
## Versioning & supported releases
- We follow [Semantic Versioning](https://semver.org/). In the `0.y.z`
pre-release series, minor-version bumps (`0.4 → 0.5`) may include breaking
API changes; patch bumps (`0.4.0 → 0.4.1`) will not. Starting with `1.0.0`,
the standard semver contract applies: only major bumps (`1.x → 2.0`) may
break the public API.
- Security fixes are issued on the latest minor line. We do not currently
backport to older `0.x` lines; after `1.0.0` we will evaluate backports
case-by-case for high-severity findings.
- The MSRV (currently 1.85) may be bumped in any minor release in the `0.x`
series. After `1.0.0`, MSRV bumps will be treated as minor-version changes
and documented in the changelog.
---
## Reporting a vulnerability
**Do not** open a public GitHub issue for security bugs.
Use **GitHub Private Vulnerability Reporting** on the
[`tenuo-ai/path_jail` repository](https://github.com/tenuo-ai/path_jail)
(Security tab → "Report a vulnerability"), or email **security@tenuo.ai**
with:
- A minimal reproducer (Rust code that demonstrates the issue)
- The affected `path_jail` version and feature flags
- The platform (OS, kernel version, architecture)
- Your assessment of the impact (information disclosure / write outside jail
/ etc.)
We aim to acknowledge within 5 business days and to ship a fix within 30
days for high-severity findings (escape from a documented containment
guarantee). Lower-severity findings (e.g., a missing defense for a
documented out-of-scope threat) will be triaged on the open repository.
---
## A note on the `Jail` default API
The README quick-start uses `Jail::new` and `Jail::join` — the path-based
API. That API is **not TOCTOU-safe**. It validates a path string and returns
a `PathBuf`; whatever the caller does with that `PathBuf` is a separate
operation with its own race window.
This is documented but easy to miss. If you operate in any of these
environments, **strongly prefer `guard`** over the path-based API:
- Multi-tenant systems where another local process can manipulate the
filesystem
- File-upload paths where the same directory is also writable by other
workers
- Anywhere "the file you validated" and "the file you opened" need to be
the same file with certainty
We may make `guard` the default in `1.0.0` or a future major release. For now,
choose explicitly.