# Pi-Web File Access Security: How It Scopes Browsing to Allowed Project Directories

> Learn how Pi-web file access security restricts browsing to allowed project directories by using a whitelist to enforce safe directory roots and grant access only to matching or nested paths.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: security
- Published: 2026-07-31

---

**Pi-web restricts every file-listing and preview request to a dynamically constructed whitelist of safe directory roots, enforcing that all target paths must either match or reside within these allowed roots before granting access.**

Pi-web implements a defense-in-depth file access security model to ensure that file browsing operations remain confined to legitimate project directories. According to the agegr/pi-web source code, the system aggregates a whitelist of permitted filesystem locations from session metadata, project configurations, and explicit runtime registrations. This whitelist is then enforced by path resolution functions that validate every file request against the allowed set, preventing unauthorized traversal into sensitive system locations.

## How the Allowed Roots Whitelist Is Constructed

The security model centers on a whitelist of directory roots that are considered safe for file operations. In [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts), the `getAllowedFileRoots()` function assembles this list by scanning four distinct sources.

### Session Working Directories and Project Roots

The primary sources of allowed roots are the active Pi sessions stored on disk. The system invokes `listAllSessions()` from [`lib/session-reader.ts`](https://github.com/agegr/pi-web/blob/main/lib/session-reader.ts) to enumerate all session files. For each session, the code extracts the `cwd` (current working directory) and `projectRoot` properties (accessed as `s.projectRoot`), normalizes them using `normalizeSlashes`, and adds them to the global whitelist set. This ensures that files within any active session's working directory are immediately accessible.

### Temporary CWD Directories

Pi-web automatically includes temporary directories created by the default-cwd endpoint. These folders follow the naming pattern `~/pi-cwd-YYYYMMDD` and are discovered by scanning the user's home directory with `readdirSync(homedir())`. Any directory matching the regex `/^pi-cwd-\d{8}$/` is added to the allowed roots, enabling access to transient project workspaces without manual configuration.

### Explicit Runtime Registrations

Applications and plugins can expand the whitelist at runtime through the `allowFileRoot()` function exported from [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts). This helper pushes a normalized path into the global `__piAdditionalAllowedRoots` set and immediately invalidates the `__piAllowedRootsCache`, allowing new directories to be accessed without requiring a server restart.

## Path Validation and Enforcement Logic

Once the whitelist is established, pi-web enforces access control through two main validation functions defined in [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts).

The core security function `isFilePathAllowed(target, allowedRoots)` implements the following validation sequence:

1. Iterates over every root in the whitelist.
2. Detects Windows-style paths using `isWindowsAbsolutePath` to determine appropriate path resolution strategy.
3. Resolves both the target and root to absolute, normalized paths using `path.resolve` (or `path.win32.resolve` on Windows).
4. Performs case-insensitive comparison on Windows and case-sensitive comparison on Unix-like systems.
5. Grants access only if the target exactly matches the root or starts with the root followed by a path separator (`root/…`).

If no roots satisfy these conditions, the function returns `false` and the API endpoint aborts the request.

### Symlink Attack Mitigation

For enhanced security against symbolic link traversal attacks, the `isExistingFilePathAllowed` wrapper performs additional validation. It calls `isExistingPathWithinRoots` from [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts), which resolves all symbolic links in the target path before performing the containment check. This guarantees that a malicious symlink cannot escape the allowed directory tree by pointing to sensitive locations outside the whitelist.

## Cross-Platform Compatibility and Caching

The implementation handles platform differences and performance optimization through specific architectural decisions.

On Windows systems, the code detects absolute paths (such as `C:\…` or UNC paths) and uses `path.win32` for resolution, ensuring consistent security behavior across operating systems. The path comparison logic automatically adjusts case sensitivity based on the platform, preventing bypasses through case manipulation on Windows file systems.

To maintain performance without sacrificing security, pi-web caches the allowed roots set for 5 seconds (`ALLOWED_ROOTS_TTL_MS`). The cache resides on `globalThis`, which persists across Next.js hot-reloads, preventing unnecessary filesystem scans during rapid development iterations while ensuring the whitelist remains current.

## Practical Implementation Examples

The following patterns demonstrate how to integrate pi-web's file access security into application code:

```typescript
// Validating a file read request against allowed roots
import { getAllowedFileRoots, isFilePathAllowed } from "./lib/file-access";

async function canReadFile(requestedPath: string): Promise<boolean> {
  const roots = await getAllowedFileRoots();       // builds the whitelist
  return isFilePathAllowed(requestedPath, roots); // true → safe, false → reject
}

```

To dynamically expand access permissions at runtime, use the `allowFileRoot` API:

```typescript
// Adding an extra root at runtime (e.g., a plugin requires /var/log)
import { allowFileRoot } from "./lib/allowed-roots";

allowFileRoot("/var/log");   // now any file under /var/log is readable

```

## Summary

- Pi-web constructs a dynamic whitelist of allowed directory roots from session `cwd` values, project roots, temporary `pi-cwd-YYYYMMDD` directories, and explicit runtime registrations via `allowFileRoot()`.
- The `isFilePathAllowed` function in [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) enforces containment by requiring targets to match or reside within whitelisted roots, with platform-specific handling for Windows and Unix paths.
- Symbolic link attacks are mitigated by `isExistingPathWithinRoots` in [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts), which resolves links before validation to prevent directory traversal.
- A 5-second TTL cache stored on `globalThis` optimizes performance while preserving security boundaries across Next.js hot-reloads.
- Runtime calls to `allowFileRoot()` immediately update the global `__piAllowedRootsCache`, enabling dynamic security boundary expansion without server restarts.

## Frequently Asked Questions

### How does pi-web prevent access to files outside project directories?

Pi-web maintains a whitelist of allowed filesystem roots and validates every file request against this list using `isFilePathAllowed` in [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts). The function resolves both the requested path and each allowed root to absolute, normalized forms using `path.resolve`, then checks if the target either matches a root exactly or starts with a root followed by a path separator. If no match is found, the request is rejected before any filesystem read occurs.

### What protection exists against symbolic links that point outside allowed roots?

The system uses `isExistingFilePathAllowed`, a security wrapper that calls `isExistingPathWithinRoots` from [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts). This function resolves all symbolic links in the target path to their ultimate destinations before performing the containment check against the whitelist, ensuring that a symlink cannot bypass security by pointing to locations outside the allowed project directories.

### Can new directories be added to the whitelist without restarting the server?

Yes. The `allowFileRoot()` function exported from [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts) enables runtime expansion of the whitelist. When called, it adds the specified path to the global `__piAdditionalAllowedRoots` set and updates the `__piAllowedRootsCache` immediately, allowing UI components or plugins to register new accessible directories on demand without requiring a process restart.

### How does the caching mechanism affect security enforcement?

The allowed roots are cached for 5 seconds (`ALLOWED_ROOTS_TTL_MS`) to reduce filesystem overhead from repeated session scans. The cache is stored on `globalThis`, which survives Next.js hot-reloads. This caching does not compromise security because the whitelist is rebuilt from authoritative sources—session files, project configurations, and explicit registrations—on each cache miss, ensuring that revoked access is enforced within the TTL window.