# How Pi Web's File Browser Prevents Path Traversal Attacks

> Learn how Pi Web's file browser prevents path traversal attacks using root directory whitelisting, lexical containment, and real-path resolution for secure file access.

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

---

**Pi Web neutralizes path traversal attacks by enforcing a strict whitelist of allowed directory roots combined with platform-aware lexical containment checks and real-path resolution for symbolic links.**

The `agegr/pi-web` repository implements a multi-layered security architecture in its file browser. Rather than exposing the entire filesystem to users, every file access request undergoes rigorous validation through specialized modules before any OS-level filesystem operations occur.

## How the Allowed Roots Whitelist Works

The foundation of Pi Web's security model is a **dynamically constructed whitelist**. The `getAllowedFileRoots()` function in [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) aggregates permitted directories from multiple sources:

- The current working directory of every active session
- The project root directory
- Any `~/pi-cwd-*` directories in the user's home
- Additional roots configured through the API ([`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts))

This collection process is **cached for 5 seconds** to prevent performance degradation from repeated filesystem scans. The resulting array of absolute paths forms the security boundary that no user request can escape.

## Lexical Containment Checking

Before any file operation, the `isPathWithinRoots()` function in [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts) performs strict lexical validation:

1. **Platform-specific resolution** — Uses POSIX or Windows path resolvers appropriate to the runtime environment
2. **Case normalization** — Handles Windows case-insensitivity correctly
3. **Separator standardization** — Eliminates mixed-separator attack vectors
4. **Prefix validation** — Confirms the target equals a root or starts with a root followed by a path separator

This approach neutralizes `../` sequences, encoded traversal sequences, and separator manipulation attempts purely through string analysis—**before any filesystem access occurs**.

## Symbolic Link Protection

For operations requiring path existence verification, `isExistingPathWithinRoots()` adds an additional hardening layer. This function in [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts):

- Resolves the **real canonical path** of both target and roots using `realpathSync`
- Performs containment checks against these resolved paths
- Blocks symlink-based escapes that lexical checks alone cannot detect

This ensures that even if an attacker places a symbolic link inside an allowed directory pointing outside the whitelist, the resolved target path is still validated against permitted roots.

## API-Level Enforcement

Every file-access endpoint in Pi Web's API enforces these checks. Route handlers in files like `app/api/files/[...path]/route.ts` implement the following pattern:

```typescript
import { getAllowedFileRoots, isFilePathAllowed } from "../../../lib/file-access";

// Inside API route handler
export async function GET(req: Request) {
  const path = extractPath(req);
  
  if (!(await isFilePathAllowed(path, await getAllowedFileRoots()))) {
    return new Response(JSON.stringify({ error: "Access denied" }), { 
      status: 403 
    });
  }
  
  // Only then proceed to filesystem access
  // ...
}

```

For endpoints that verify file existence before operations:

```typescript
import { getAllowedFileRoots, isExistingFilePathAllowed } from "../../../lib/file-access";

// Validates path AND resolves symlinks before access
const allowed = await isExistingFilePathAllowed(targetPath, await getAllowedFileRoots());
if (!allowed) {
  throw new ForbiddenError("Path outside allowed directory");
}

```

## Practical Implementation Examples

### Implementing a Secure File Read

```typescript
import { getAllowedFileRoots, isExistingFilePathAllowed } from "./lib/file-access";
import { readFileSync } from "fs";

async function secureReadFile(userRequestedPath: string): Promise<string> {
  // Always use isExistingFilePathAllowed for actual file operations
  // to catch symlink attacks
  if (!(await isExistingFilePathAllowed(userRequestedPath, await getAllowedFileRoots()))) {
    throw new Error("Access denied: path outside allowed directories");
  }
  
  // Safe to read—path is whitelisted and symlinks resolved
  return readFileSync(userRequestedPath, "utf8");
}

```

### Adding Custom Allowed Roots

```typescript
import { addAllowedRoot } from "./lib/allowed-roots";

// Register additional directory for plugin access
addAllowedRoot("/secure/plugins/data");

// Now included in all subsequent getAllowedFileRoots() calls

```

## Security Architecture Overview

| Component | File Path | Security Function |
|-----------|-----------|-------------------|
| Root aggregation | [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) | `getAllowedFileRoots()` — builds dynamic whitelist |
| Lexical containment | [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts) | `isPathWithinRoots()` — string-level path validation |
| Symlink resolution | [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts) | `isExistingPathWithinRoots()` — real-path verification |
| API helpers | [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) | `isFilePathAllowed()`, `isExistingFilePathAllowed()` |
| Extension points | [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts) | `addAllowedRoot()` — programmatic root registration |

## Summary

Pi Web's file browser security relies on three core mechanisms working in concert:

- **Dynamic root whitelisting** limits filesystem exposure to explicitly permitted directories
- **Lexical containment validation** blocks traversal attempts through path manipulation
- **Real-path resolution** closes the symbolic link bypass vulnerability

These checks execute at the application layer before any OS filesystem calls, ensuring path traversal attacks are intercepted regardless of operating system or platform-specific path behaviors.

## Frequently Asked Questions

### How does Pi Web handle case-insensitive filesystems like Windows?

The `isPathWithinRoots()` function in [`lib/path-security.ts`](https://github.com/agegr/pi-web/blob/main/lib/path-security.ts) normalizes case when running on Windows, converting both target paths and allowed roots to consistent casing before comparison. This prevents case-mismatch bypasses while maintaining correct behavior on case-sensitive systems.

### Can symbolic links inside allowed directories access outside files?

No. When `isExistingFilePathAllowed()` is used (required for actual file reads), the function calls `realpathSync` on both the target and all allowed roots. The containment check operates on these fully resolved canonical paths, so a symlink pointing outside the whitelist resolves to its true destination—which fails the root validation.

### What happens if `getAllowedFileRoots()` fails or returns empty?

The security model defaults to **deny-all**. Both `isFilePathAllowed()` and `isExistingFilePathAllowed()` return `false` when the roots array is empty or the target path cannot be validated. All API endpoints treat validation failure as an access denial, returning HTTP 403 before attempting any filesystem operation.

### How can I add temporary directories to the allowed roots?

Use the `addAllowedRoot()` function exported from [`lib/allowed-roots.ts`](https://github.com/agegr/pi-web/blob/main/lib/allowed-roots.ts). Roots added through this API are immediately included in subsequent `getAllowedFileRoots()` calls. For session-scoped temporary access, store the path in a `~/pi-cwd-*` directory pattern, which `getAllowedFileRoots()` automatically discovers.