How the File Access Allow-List Security Boundary Works in /api/files
The /api/files endpoint enforces a strict file access allow-list security boundary that restricts all filesystem operations to directories derived from active sessions, temporary workspace folders, and explicitly approved roots, using canonical path validation and symlink resolution to prevent unauthorized access.
The agegr/pi-web repository implements a defense-in-depth strategy for its file management API. This file access allow-list security boundary ensures that the /api/files endpoint can only interact with files residing in pre-approved locations, effectively sandboxing filesystem access against directory traversal attacks.
How the Allow-List Is Constructed
The security boundary begins with getAllowedFileRoots() in lib/file-access.ts, which aggregates trusted directories from three distinct sources into a Set<string>.
Session Working Directories and Project Roots
For every active session retrieved via listAllSessions(), the function extracts both the working directory (cwd) and the resolved projectRoot. These paths are normalized and added to the allow-list set (lines 29-36).
Temporary pi-cwd Directories
The scanner searches the user's home folder for directories matching the pattern pi-cwd-\d{8}. These temporary workspaces are automatically included in the boundary to support ad-hoc scratch folders created by the default-cwd endpoint (lines 38-44).
Runtime-Added Roots via allowFileRoot()
Any runtime component can extend the boundary by calling allowFileRoot(path) from lib/allowed-roots.ts. This function stores normalized paths in a global __piAdditionalAllowedRoots set and invalidates the cached roots map, enabling dynamic expansion without server restarts (lines 19-24).
Five-Second Cache Strategy
To balance security with performance, the aggregated roots are stored in __piAllowedRootsCache with a time-to-live (TTL) of 5 seconds. This short cache duration ensures newly created sessions or dynamically added roots are recognized quickly while avoiding expensive filesystem scans on every request (lines 9-13, 17-18).
Path Validation Logic
When a request hits app/api/files/[...path]/route.ts, the handler resolves the requested path and evaluates it against the cached allow-list through a multi-layered validation sequence.
Canonical Prefix Comparison
The primary check uses isFilePathAllowed() (lines 55-66) to perform a case-aware prefix comparison after converting both the target path and each allowed root to absolute, normalized paths (POSIX-style / separators). If the requested path starts with any allowed root, the operation proceeds.
Session Reference Fallback
For read operations that fail the root check but are not directory listings, the system invokes isFilePathReferencedBySession() from lib/session-file-references.ts. This inspects the session's .jsonl file to verify if the path appears as an explicit reference (e.g., an uploaded file), permitting access to session-specific attachments outside the standard roots.
Symlink Resolution for Write Operations
Before any destructive operation (upload, delete), the handler calls isExistingFilePathAllowed(), which delegates to isExistingPathWithinRoots() in lib/path-security.ts. This resolves the absolute real path by following symlinks and re-applies the prefix check, preventing symlink-based directory traversal attacks that could escape the allow-list (lines 72-75).
Practical Implementation Examples
Listing a directory within an allowed session root:
curl "http://localhost:30141/api/files/home/user/project?type=list&sessionId=12345"
# → { "entries": [...], "path": "/home/user/project" }
Reading a file referenced by the session:
curl "http://localhost:30141/api/files/home/user/project/file.txt?type=read&sessionId=12345"
# → { "content": "...", "language": "text", "size": 428 }
Attempting to access a system file outside the boundary:
curl "http://localhost:30141/api/files/etc/passwd?type=read"
# → { "error": "Access denied" } # HTTP 403
Dynamically adding a custom root at runtime:
import { allowFileRoot } from '@/lib/allowed-roots';
allowFileRoot('/mnt/shared'); // Added to __piAdditionalAllowedRoots
// Subsequent uploads to /mnt/shared are now permitted
Summary
- The file access allow-list security boundary aggregates allowed directories from session roots, temporary
pi-cwd-*folders, and explicit runtime additions viaallowFileRoot(). - A 5-second TTL cache (
__piAllowedRootsCache) stores the aggregated roots inlib/file-access.tsto minimize filesystem overhead while maintaining freshness. - All paths undergo canonical normalization and prefix matching through
isFilePathAllowed()before any operation proceeds. - Session references provide a fallback validation for files explicitly attached to a session but located outside standard roots.
- Symlink resolution via
isExistingPathWithinRoots()inlib/path-security.tsprevents traversal attacks during write operations.
Frequently Asked Questions
How does the system handle newly created sessions?
The 5-second cache TTL ensures that sessions created after the server starts are incorporated into the allow-list within a few seconds. When getAllowedFileRoots() detects a stale cache, it rebuilds the set by scanning all current sessions and their project roots.
Can the allow-list be modified without restarting the server?
Yes. The allowFileRoot() function in lib/allowed-roots.ts dynamically adds paths to the global __piAdditionalAllowedRoots set and clears the cached roots map, allowing plugins or runtime configuration to extend the security boundary immediately.
What prevents symbolic links from bypassing the allow-list?
For any operation that modifies the filesystem, the handler calls isExistingFilePathAllowed(), which resolves the absolute real path (following symlinks) using isExistingPathWithinRoots() from lib/path-security.ts. The prefix check is then applied to the resolved path, ensuring symlinks cannot point outside allowed directories.
Why is there a session reference fallback for read operations?
The isFilePathReferencedBySession() check allows users to read files that were explicitly uploaded or referenced in their session's .jsonl file, even if those files reside outside the standard session roots. This supports temporary file attachments while maintaining the security boundary for directory listings and write operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →