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

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

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.
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.
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.
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.
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.
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

Error Handling

Blocked Attempts

The following path segments are all blocked when the base is /data/uploads:

Integration Points

safePath() is used throughout the codebase wherever file system operations accept user-controlled path segments:
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

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