# How Pi-Web Implements File Access Security Boundaries and Allow-Lists

> Learn how Pi-Web secures file access with dynamic allow-lists, preventing traversal attacks through session directories, project roots, and more. Discover its robust containment strategy.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-07-26

---

**Pi-Web enforces strict filesystem containment through a dynamic allow-list cached for 5 seconds, combining session working directories, project roots, temporary user CWDs, and runtime extensions, with path normalization and symlink-aware validation to prevent traversal attacks.**

The `agegr/pi-web` repository protects its `/api/files` endpoints by maintaining an explicit **file access security boundary** rather than granting broad filesystem access. This security model dynamically aggregates allowed paths from multiple sources and validates every file request against a short-lived cache to balance performance with real-time updates.

## The Allow-List Architecture

Pi-Web constructs its allow-list inside [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) by aggregating four distinct sources of trusted directories. When `getAllowedFileRoots` executes, it compiles these roots into a comprehensive containment set.

### Session Working Directories and Project Roots

For every stored Pi session, the system automatically includes both the current working directory (`cwd`) and the top-level `projectRoot`. These are collected during cache construction:

```typescript
roots.add(normalizeSlashes(s.cwd))
roots.add(normalizeSlashes(s.projectRoot))

```

According to the source code at lines 31-36 of [`file-access.ts`](https://github.com/agegr/pi-web/blob/main/file-access.ts), these paths represent the foundational security boundaries derived from active development sessions and their backing repositories.

### Temporary User Directories

The scanner also detects directories matching the pattern `~/pi-cwd-YYYYMMDD` created by the *default-cwd* endpoint. These are discovered by scanning the user’s `$HOME` directory:

```typescript
roots.add(normalizeSlashes(path.join(homedir(), name)))

```

This logic appears at lines 40-44 of [`file-access.ts`](https://github.com/agegr/pi-web/blob/main/file-access.ts), ensuring that user-created temporary working directories are automatically incorporated into the security perimeter.

### Runtime Extensions

Plugins and UI components can expand the allow-list dynamically via `allowFileRoot`. These additional roots are stored in a global Set managed by [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts) and merged during cache generation:

```typescript
getAdditionalAllowedRoots().add(normalizedRoot)

```

As implemented at lines 19-23 of [`allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/allowed-roots.ts), this mechanism provides **runtime extensibility** without requiring server restarts.

## Cache Implementation and TTL

To avoid expensive session database scans on every request, Pi-Web caches the computed allow-list on `globalThis.__piAllowedRootsCache` with a **5-second TTL** (5000 ms). The implementation at lines 9-13 and 24-28 of [`file-access.ts`](https://github.com/agegr/pi-web/blob/main/file-access.ts) uses this global object specifically to survive Next.js hot-reloads during development.

## Path Validation and Security Checks

When a request targets a specific file, Pi-Web validates access using `isFilePathAllowed` (lines 57-68 of [`file-access.ts`](https://github.com/agegr/pi-web/blob/main/file-access.ts)). The function performs three critical security operations:

1. **Path Normalization** – Resolves both target and root paths to absolute form using `path.win32` for Windows paths or the default Node `path` module (lines 57-62)
2. **Case-Insensitive Comparison** – Normalizes paths to lower-case when Windows is detected to handle case-insensitive filesystems correctly (lines 62-64)
3. **Containment Verification** – Validates that the target is either exactly the root directory or starts with the root path plus a path separator, preventing traversal via `../` sequences (lines 64-68)

## Symlink Handling

For operations that must follow symbolic links, Pi-Web delegates to `isExistingPathWithinRoots` (exposed from [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts)). As referenced at lines 73-75 of [`file-access.ts`](https://github.com/agegr/pi-web/blob/main/file-access.ts), this routine resolves the real path using `fs.realpath` and then applies the same containment logic. This ensures that even if a symlink points outside the allowed roots, the resolved target is still validated against the security boundary.

## Practical Implementation Examples

To verify a user-supplied path before processing:

```typescript
import { getAllowedFileRoots, isFilePathAllowed } from '@/lib/file-access';

async function canReadPath(requestedPath: string): Promise<boolean> {
  const allowedRoots = await getAllowedFileRoots(); // Uses 5-second cache
  return isFilePathAllowed(requestedPath, allowedRoots);
}

```

For server-side endpoint validation:

```typescript
export async function GET(req: Request) {
  const { path } = await req.json();
  const allowedRoots = await getAllowedFileRoots();

  if (!isFilePathAllowed(path, allowedRoots)) {
    return new Response('Forbidden', { status: 403 });
  }
  // Safe to proceed with file read
}

```

To dynamically expand access for a plugin:

```typescript
import { allowFileRoot } from '@/lib/file-access';

// Expose a shared assets folder from a custom plugin
allowFileRoot('/srv/shared/assets');

```

## Summary

- Pi-Web uses an **explicit allow-list** derived from session data, project roots, temporary CWDs (`~/pi-cwd-YYYYMMDD`), and runtime extensions to restrict filesystem access
- The allow-list is **cached for 5 seconds** on `globalThis.__piAllowedRootsCache` to optimize performance while allowing dynamic updates
- **Path traversal protection** is enforced through normalization, case-insensitive comparison on Windows, and strict containment checks requiring the target to start with an allowed root plus a separator
- **Symbolic links** are secured by resolving real paths before validation using `isExistingPathWithinRoots` in [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts)
- **Runtime extensibility** is supported via `allowFileRoot` in [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts) for plugins requiring additional directory access

## Frequently Asked Questions

### How does Pi-Web prevent path traversal attacks?

Pi-Web normalizes all paths to absolute form using the appropriate Node `path` module and validates that the requested file either exactly matches an allowed root or resides within it by verifying the path starts with the root plus a path separator. This containment logic, implemented in `isFilePathAllowed`, prevents `../` sequences from escaping permitted directories regardless of how the path is constructed.

### Why is the allow-list cached for only 5 seconds?

The 5-second TTL (5000 ms) defined in [`file-access.ts`](https://github.com/agegr/pi-web/blob/main/file-access.ts) balances performance against freshness. It avoids expensive database scans and directory listings on every API request while ensuring that new sessions, worktrees, or programmatically added roots become active within seconds without requiring manual cache invalidation.

### Can plugins add their own allowed directories?

Yes. Plugins and UI components can call `allowFileRoot('/path/to/dir')` from [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts) to register additional roots at runtime. These extensions are stored in a global Set merged during `getAllowedFileRoots` execution, and changes reflect in the security boundary within the cache's 5-second TTL window.

### How does the system handle case-insensitive filesystems on Windows?

When `isFilePathAllowed` detects a Windows path (using `path.win32`), it normalizes both the target and allowed roots to lower-case before comparison, as shown at lines 62-64 of [`file-access.ts`](https://github.com/agegr/pi-web/blob/main/file-access.ts). This ensures that paths like `C:\Project\File.txt` and `c:\project\file.txt` are treated identically for security validation, preventing bypasses through case variation.