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.

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 via allowFileRoot().
  • A 5-second TTL cache (__piAllowedRootsCache) stores the aggregated roots in lib/file-access.ts to 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() in lib/path-security.ts prevents 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →