npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

secure-path

v1.0.1

Published

TypeScript-powered secure path resolution with browser support - prevents path traversal attacks

Readme

secure-path

Socket Badge Quality Gate Status Security Rating Vulnerabilities npm version TypeScript Node.js Browser License: MIT

Securely resolve paths within a trusted directory. Addresses CWE-73 (External Control of File Name or Path) by enforcing strict path validation and containment within a trusted base directory.

Works in both Node.js and browsers with TypeScript support.

Features

  • 🛡️ Path Traversal Protection: Prevents ../ and absolute path escapes
  • 🌐 Browser Support: Frontend validation before backend processing
  • 🔒 Symlink Security: Detects and prevents symlink-based escapes (Node.js)
  • 🚫 Blacklist Patterns: Block sensitive paths with glob patterns (.env, .ssh/, secrets/)
  • 🏭 Factory Helper: Optional convenience wrapper reduces boilerplate by ~50%
  • 📝 TypeScript First: Written in TypeScript with branded types
  • Windows Support: Handles reserved names and device paths
  • 📦 Zero Dependencies: No external runtime dependencies
  • 🎯 Intent-Based API: Three specialized functions for different use cases
  • Fast: Optimized two-layer architecture
  • 📊 Multiple Formats: ESM, CJS, IIFE, UMD builds included

Installation

npm install secure-path

Quick Start

Browser (Frontend Validation)

<!-- Via CDN (IIFE) -->
<script src="https://unpkg.com/secure-path/dist/browser.iife.js"></script>
<script>
  const result = SecurePath.validatePathSyntax(userInput);
  if (result.valid) {
    // Send sanitized path to backend
    fetch('/api/files', {
      method: 'POST',
      body: JSON.stringify({ path: result.sanitized })
    });
  } else {
    console.error('Invalid path:', result.error);
  }
</script>
// ES Modules (bundler/modern browsers)
import { validatePathSyntax } from 'secure-path/browser';

// Pre-validate on frontend (UX improvement)
const result = validatePathSyntax(userInput);
if (result.valid) {
  // Send to backend for real validation
  await submitPath(result.sanitized);
}

Node.js (Backend - Full Validation)

import {
  resolveLexicalPath,
  resolveExistingPath,
  resolveNewPath
} from 'secure-path';

const baseDir = '/var/www/uploads';

// Three intent-based functions (replaces mode parameter)

// 1. Lexical validation only (no filesystem access)
const lexicalPath = resolveLexicalPath(userInput, baseDir);

// 2. Validate existing file/directory
const existingPath = resolveExistingPath(userInput, baseDir);

// 3. Validate path for new file creation
const newPath = resolveNewPath(userInput, baseDir);

// All return PathResolutionResult with metadata
console.log(existingPath.path);      // ValidatedPath (branded type)
console.log(existingPath.normalized); // Human-readable path
console.log(existingPath.isSymlink);  // Symlink detection
console.log(existingPath.realPath);   // After symlink resolution

Secure I/O Helpers (TOCTOU-Aware)

import { readSecureFile, writeSecureFile, FilePermission } from 'secure-path';

// TOCTOU-aware: reduces race window via descriptor verification
const content = await readSecureFile(userInput, baseDir);

// Overwrite mode: verify-before-truncate (narrows race window)
await writeSecureFile(userInput, baseDir, content);

// Exclusive create: O_EXCL — strongest protection for new files
await writeSecureFile(userInput, baseDir, content, {
  exclusive: true,
  mode: FilePermission.OWNER_READ_WRITE  // 0o600
});

Factory Helper (Optional - Reduces Boilerplate)

import { createResolver } from 'secure-path';

// Create resolver with pre-configured base and policy
const resolver = createResolver('/app/data', {
  maxPathLength: 2048,
  blacklistedPaths: ['**/.ssh/**', '*.env', '**/secrets/**']
});

// All methods pre-configured with base directory
const result = resolver.resolveLexical('file.txt');
const content = await resolver.readFile('config.json');
await resolver.writeFile('output.txt', 'data');

// Works with destructuring
const { resolveLexical, readFile, writeFile } = resolver;

Two-Layer Architecture

Layer 1: Browser String Validation (No filesystem)

  • Pure string validation
  • Path traversal detection (../, absolute paths)
  • Character validation
  • Works in browsers
  • Fast client-side feedback

Layer 2: Node.js Full Validation (With filesystem)

  • All Layer 1 checks PLUS:
  • Real filesystem verification
  • Symlink resolution and validation
  • Parent directory existence checks
  • TOCTOU-aware I/O operations (descriptor-based verification)

Security Guarantees

Path Validation

  • No false positives: ... filenames are valid
  • Null byte rejection: Prevents C-level string truncation attacks
  • Path length limits: Default 4096 chars (prevents DoS)
  • Traversal detection: Segment-based validation (no regex bypasses)
  • Device path blocking: Win32 namespace, named pipes, UNC paths
  • Reserved names: Windows COM1-COM9, LPT1-LPT9, CON, PRN, etc.
  • 8.3 short name blocking: PROGRA~1 blacklist bypass prevention
  • ADS detection: NTFS Alternate Data Streams (file.txt:hidden:$DATA)
  • Trailing dots/spaces: Windows-specific blacklist bypass prevention

Unicode Safety

  • Bidirectional override detection: RLO/LRO display spoofing (file\u202Eexe.txt)
  • Zero-width character detection: ZWSP, ZWJ, ZWNJ, BOM
  • Homograph detection: Mixed Latin + Cyrillic scripts (аdmin vs admin)

Error Security

  • Log injection prevention: Auto-sanitized error messages
  • Prototype pollution: Options validated with Object.hasOwn()
  • ANSI escape neutralization: Terminal control codes escaped

Testing

  • 472 tests across 22 suites
  • 95%+ coverage target (lines), 90%+ (branches)
  • Platform-tested on Windows and POSIX

See CHANGELOG.md for detailed changes.

Vulnerability Coverage

This library mitigates vulnerabilities flagged by major SAST scanners. Full details in Security Reference.

CWE Coverage (11 weaknesses)

| CWE | Name | ErrorReason | |-----|------|-------------| | CWE-22 | Path Traversal | traversal_detected | | CWE-23 | Relative Path Traversal | traversal_detected | | CWE-36 | Absolute Path Traversal | absolute_not_allowed, device_path, reserved_name | | CWE-59 | Improper Link Resolution | symlink_escape, symlink_loop, broken_symlink | | CWE-61 | UNIX Symlink Following | symlink_escape | | CWE-158 | Null Byte Injection | null_byte | | CWE-400 | Resource Consumption | path_too_long, symlink_loop | | CWE-434 | Unrestricted File Upload | invalid_extension | | CWE-552 | Accessible Sensitive Files | blacklisted_path | | CWE-1321 | Prototype Pollution | invalid_type |

SAST Rules Coverage

| Rule | Mitigation | |------|------------| | javascript.lang.security.audit.path-traversal.path-join-resolve-traversal | resolveLexicalPath validates before resolving | | javascript.lang.security.audit.path-traversal.express-path-join | Replace path.join(dir, req.params) with resolveExistingPath() | | javascript.lang.security.audit.path-traversal.path-traversal-join | All resolve functions validate containment | | javascript.lang.security.audit.path-traversal.path-traversal-non-literal-fs-filename | Use readSecureFile() / writeSecureFile() | | javascript.lang.security.audit.prototype-pollution.prototype-pollution-assignment | validateOptionsObject() blocks __proto__ | | javascript.lang.security.audit.prototype-pollution.prototype-pollution-loop | validateOptionsObject() recursive depth check | | javascript.lang.security.detect-non-literal-fs-filename | I/O helpers validate before every fs call | | javascript.lang.security.audit.detect-non-literal-require | resolveLexicalPath validates paths |

| Rule | Mitigation | |------|------------| | js/path-injection | All resolve functions validate containment | | js/zipslip | resolveLexicalPath blocks ../ in extracted names | | js/prototype-polluting-assignment | validateOptionsObject() |

| Rule | Mitigation | |------|------------| | S2083 | Path traversal injection — 5-layer detection | | S5144 | Server-side path injection — all resolve functions | | S4507 | Debug features in production — error messages sanitized |

| Rule | Mitigation | |------|------------| | security/detect-non-literal-fs-filename | I/O helpers wrap all fs calls | | security/detect-object-injection | getOwnProperty() safe accessor |

| Attack | Mitigation | |--------|------------| | Path Traversal | 5-layer detection (../, ..%2f, %252e, ....//, lexical + real path) | | Embedding Null Code | Null byte rejection at validation layer | | Unrestricted File Upload | Extension allowlist via allowedExtensions |

| CWE | Class | Mitigation | |-----|-------|------------| | CWE-73 | External Control of File Name or Path | Core threat model — all resolve functions enforce containment within trusted base directory | | CWE-367 | TOCTOU Race Condition | Uses descriptor-based verification (fstat vs lstat) and O_NOFOLLOW where supported to detect certain races and reduce the TOCTOU window. Note: O_NOFOLLOW is only enforced on platforms that support it (Linux/macOS). On Windows, protection is reduced. See also: Race Conditions (PortSwigger) |

Full mapping with code examples: docs/security-reference.md

⚠️ Security Warning

Browser validation is for UX only! It can be bypassed by attackers. Always re-validate on the backend:

// ❌ INSECURE - Never trust client validation alone
app.post('/upload', (req, res) => {
  fs.writeFileSync(req.body.path, req.body.content); // NO!
});

// ✅ SECURE - Always validate on backend
app.post('/upload', async (req, res) => {
  const safePath = resolveNewPath(req.body.path, BASE_DIR);
  await writeSecureFile(safePath.path, BASE_DIR, req.body.content);
});

Documentation

Complete documentation is available in the /docs directory:

Advanced Features

Blacklist Path Patterns

Block sensitive paths using glob patterns for defense-in-depth security:

import { resolveLexicalPath } from 'secure-path';

const policy = {
  blacklistedPaths: [
    '**/.ssh/**',      // SSH keys and config
    '**/secrets/**',   // Secret directories
    '*.env',           // Environment files
    '**/.env.*',       // .env variants (.env.local, etc)
    '**/.git/**',      // Git internals
    '**/private/**',   // Private data
    '**/*.key',        // Key files
    '**/*.pem'         // Certificates
  ]
};

// Throws PathValidationError if path matches any pattern
const result = resolveLexicalPath(userInput, '/app/data', { policy });

Pattern Syntax:

  • * = matches any characters except / (single segment)
  • ** = matches any characters including / (recursive)
  • ? = matches exactly one character

Common Patterns:

'*.env'           // Blocks: .env, config.env, test.env
'**/.ssh/**'      // Blocks: .ssh/id_rsa, home/.ssh/config
'**/secrets/**'   // Blocks: secrets/api.key, app/secrets/token
'**/.git/**'      // Blocks: .git/config, repo/.git/HEAD

Error Handling:

try {
  resolveLexicalPath('.env', '/app', {
    policy: { blacklistedPaths: ['*.env'] }
  });
} catch (error) {
  if (error.reason === 'blacklisted_path') {
    console.log(error.message); // "Path matches blacklisted pattern: *.env"
  }
}

Factory Helper Benefits

  • Reduces Boilerplate: ~50% less code for repeated operations
  • Consistent Policy: Same security rules across all operations
  • Tree-Shakeable: Zero impact if not used
  • Type-Safe: Full TypeScript support with branded types
  • Immutable: Frozen instance prevents accidental modification

Before (Functional API):

const result1 = resolveLexicalPath('file1.txt', '/app/data', policy);
const result2 = resolveLexicalPath('file2.txt', '/app/data', policy);
const content = await readSecureFile('file3.txt', '/app/data', policy);

After (Factory API):

const resolver = createResolver('/app/data', policy);
const result1 = resolver.resolveLexical('file1.txt');
const result2 = resolver.resolveLexical('file2.txt');
const content = await resolver.readFile('file3.txt');

Quick API Reference

Three Intent-Based Functions

// 1. Fast: String validation only (no filesystem)
const result = resolveLexicalPath(userInput, baseDir);

// 2. Full: With filesystem + symlink verification
const result = resolveExistingPath(userInput, baseDir);

// 3. Create: For new file validation (parent must exist)
const result = resolveNewPath(userInput, baseDir);

// TOCTOU-aware: reduces race window via descriptor verification
const content = await readSecureFile(userInput, baseDir);
await writeSecureFile(userInput, baseDir, content);

Return Type

PathResolutionResult {
  path: ValidatedPath;        // Branded type for compile-time safety
  normalized: string;         // Human-readable path
  isSymlink: boolean;         // Symlink detection (Node.js)
  realPath: string | null;    // Resolved target (Node.js)
  metadata?: { exists, isDirectory }
}

Error Handling

try {
  const result = resolveLexicalPath(userInput, baseDir);
} catch (error) {
  if (error instanceof PathValidationError) {
    switch (error.reason) {
      case 'traversal_detected':
      case 'symlink_escape':
      case 'absolute_not_allowed':
      // ... 10 other typed error reasons
    }
  }
}

Configuration

const policy: SecurityPolicy = {
  allowAbsolute?: boolean;           // Allow absolute paths
  maxPathLength?: number;            // Default: 4096
  allowedExtensions?: string[];      // Whitelist extensions
  blacklistedPaths?: string[];       // Glob patterns to block
  caseSensitive?: boolean;           // Platform default
};

const result = resolveLexicalPath(userInput, baseDir, { policy });

Build Formats & Compatibility

| Format | Package | Use Case | |--------|---------|----------| | ESM | dist/index.mjs | Modern Node.js & bundlers | | CommonJS | dist/index.cjs | Traditional Node.js | | Browser ESM | dist/browser.esm.js | Bundlers (Webpack, Vite) | | IIFE | dist/browser.iife.js | <script> tags | | UMD | dist/browser.umd.js | AMD/CommonJS/global |

Browser import: import { validatePathSyntax } from 'secure-path/browser'

TypeScript: Full type definitions included. Branded types prevent using unvalidated paths.

Size & Performance

  • Node.js: 15 KB (ESM/CJS each)
  • Browser: 8 KB (IIFE/UMD each)
  • Lexical validation: < 1ms (string only)
  • Filesystem validation: 1-5ms (with I/O)
  • Zero dependencies

Contributing & Security

Contributions welcome! For security vulnerabilities, please email [email protected] instead of opening issues.

See Code Standards for development guidelines.

License

MIT © DungGramer