Skip to main content
The safePath() function prevents path traversal attacks by validating that resolved paths remain within a trusted base directory. It is always active with no configuration toggle — every file system operation that accepts user-controlled path segments must use this function. Path traversal is consistently ranked in the OWASP Top 10. The safePath() function is the single chokepoint for all file system path validation in the system. By centralizing this check, every file operation benefits from the same 5-layer defense without needing per-callsite validation logic.
Source: packages/core/src/security/safe-path.ts — defensive path resolution with 5 attack vector mitigations.

API

safePath

function safePath(base: string, ...segments: string[]): string
Safely resolve a path within a base directory, preventing traversal attacks. The base parameter must be an absolute path to the trusted directory. The segments are path components to join under base. Returns: The resolved, validated absolute path. Throws: PathTraversalError if the resolved path escapes the base directory.

PathTraversalError

class PathTraversalError extends Error {
  readonly name: "PathTraversalError";
  readonly base: string;      // The trusted base directory
  readonly attempted: string;  // The path that was attempted
}
Error thrown when a traversal attempt is detected. Includes both the base directory and the attempted path for diagnostics and audit logging.

Attack Vectors and Defenses

The function defends against five categories of path traversal attacks:

1. Basic Traversal (../ sequences)

Attack: Using ../ sequences to navigate above the base directory.
// Blocked: "../../../etc/passwd" resolves outside base
safePath("/data/uploads", "../../../etc/passwd");
// throws PathTraversalError
Defense: path.resolve() canonicalizes the path, then a prefix check verifies the resolved path starts with base + path.sep (or equals base exactly).

2. URL-Encoded Traversal (%2e%2e%2f)

Attack: Using URL-encoded characters to bypass naive string checks.
// Blocked: "%2e%2e%2f" decodes to "../"
safePath("/data/uploads", "%2e%2e%2fetc%2fpasswd");
// throws PathTraversalError
Defense: All segments are decoded with decodeURIComponent() before resolution. If decoding fails (malformed percent encoding), the raw segment is used instead.

3. Prefix Attacks (/uploads vs /uploads-evil)

Attack: Exploiting simple startsWith() checks where /data/uploads-evil starts with /data/uploads.
// Without trailing separator guard, this could pass a naive check
// safePath correctly blocks it
safePath("/data/uploads", "../../uploads-evil/file");
// throws PathTraversalError
Defense: The base directory is normalized with a trailing path separator before the prefix check. The resolved path must either equal base exactly or start with base + path.sep. This prevents /data/uploads-evil from matching /data/uploads.

4. Null Byte Injection

Attack: Inserting null bytes to truncate the path at the OS level while passing application-level checks.
// Blocked: null byte in segment
safePath("/data/uploads", "file.txt\0.jpg");
// throws PathTraversalError
Defense: Every segment is checked for null bytes (\0) before any path resolution. If a null byte is found, PathTraversalError is thrown immediately. Attack: Creating a symlink inside the base directory that points to a location outside it.
// If /data/uploads/link -> /etc/, this would escape:
safePath("/data/uploads", "link/passwd");
// throws PathTraversalError (symlink target checked)
Defense: After path resolution passes, each intermediate path component is checked with lstatSync(). If a component is a symbolic link, realpathSync() resolves its target and verifies the target remains within the base directory. If the symlink target escapes the base, PathTraversalError is thrown.
If an intermediate path component does not exist yet, the symlink check is skipped for that component. This is safe because non-existent paths cannot be symlinks.

Usage Patterns

Safe Usage

import { safePath } from "@comis/core";

// Resolve a user-provided filename within the uploads directory
const filePath = safePath("/data/uploads", userFilename);
// filePath is guaranteed to be under /data/uploads/

// Multiple segments are joined safely
const nested = safePath("/data/uploads", "images", "2026", userFilename);

Error Handling

import { safePath, PathTraversalError } from "@comis/core";

try {
  const resolved = safePath(baseDir, userInput);
  // Use resolved path safely
} catch (error) {
  if (error instanceof PathTraversalError) {
    // Log the traversal attempt for security audit
    logger.warn({
      base: error.base,
      attempted: error.attempted,
    }, "Path traversal blocked");
  }
}

Blocked Attempts

The following path segments are all blocked when the base is /data/uploads:
// All of these throw PathTraversalError:
safePath("/data/uploads", "../../../etc/passwd");
safePath("/data/uploads", "%2e%2e/%2e%2e/etc/passwd");
safePath("/data/uploads", "file\0.txt");
safePath("/data/uploads", "../../uploads-evil/payload");
// If /data/uploads/symlink -> /etc/
safePath("/data/uploads", "symlink/shadow");

Integration Points

safePath() is used throughout the codebase wherever file system operations accept user-controlled path segments:
ComponentUsage
File tools (file.read, file.write)Validate agent-provided file paths within workspace
Memory file storeValidate attachment paths within data directory
Skill discoveryValidate skill file paths within discovery directories
Media processingValidate temp file paths within media temp directory
Config file loadingValidate config file paths against allowed directories
Exec tool CWDValidate explicit cwd parameter within workspace
safePath() validates file tool parameters (paths passed to read, write, etc.) but cannot protect arbitrary commands passed to the exec tool. For example, exec cat /etc/passwd bypasses safePath() entirely because the path is inside a shell command string, not a tool parameter. The Exec Sandbox provides OS-level filesystem isolation to close this gap.

Defense Summary

Attack VectorDefense MechanismThrows
../ traversalpath.resolve() + prefix checkPathTraversalError
%2e%2e%2f encodeddecodeURIComponent() before resolvePathTraversalError
/uploads-evil prefixTrailing separator guard (base + path.sep)PathTraversalError
Null byte \0Segment scan before any resolutionPathTraversalError
Symlink escapelstatSync + realpathSync walkPathTraversalError

Design Notes

  • No configuration: safePath() is always active and cannot be disabled. There is no config toggle.
  • Synchronous: The function is synchronous (lstatSync, realpathSync) because path validation must complete before any file I/O operation.
  • Pure validation: The function only validates paths — it does not create files or directories.
  • Base must be absolute: The base parameter should always be an absolute path. Relative base paths would make the security guarantee dependent on the current working directory.
  • Base equals resolved: When no segments are provided (or segments resolve to base itself), the base path is returned unchanged. This is safe because the base is a trusted directory.
  • Non-existent intermediate paths: If an intermediate component does not exist on disk, the symlink check is skipped for that component. This is safe because you cannot create a symlink at a path that does not exist.
  • Re-thrown errors: If a PathTraversalError is thrown during symlink checking (e.g., the symlink target escapes base), it is re-thrown. All other file system errors (e.g., permission denied) are silently ignored since they indicate the path does not need symlink validation.
  • Thread safety: The function uses synchronous file system calls (lstatSync, realpathSync) which are safe in Node.js single-threaded event loop. No race conditions between validation and usage when called immediately before file operations.

Security Model

Defense-in-depth security architecture

Tool Security

SSRF guard, tool policies, content scanner

Sandbox

Skill sandboxing implementation