Safe Path

Path-traversal-safe multi-segment resolve. Operators consuming operator-OR-user-supplied path segments (uploaded filenames, tarball entries, archive extraction, dynamic include paths) pass base + rel to b.safePath.resolve and get back the absolute canonicalized path — guaranteed to lie strictly within base — or a typed SafePathError with a stable code on refusal.

Refusal classes (each a documented code, never best-effort):

- safe-path/absolute-rel — rel is absolute, UNC, or carries a drive letter - safe-path/escapes-base.. segments escape base after lexical resolve - safe-path/null-byte — NUL anywhere (closes Node poison-NUL class) - safe-path/control-char — C0 control char other than NUL - safe-path/bidi — bidi-override codepoint (CVE-2021-42574 Trojan Source) - safe-path/win-reserved — Windows reserved name (CON/PRN/AUX/NUL/COM0-9/LPT0-9) on EVERY platform — closes CVE-2025-27210 cross-mount class - safe-path/win-trailing — segment ends with . or under windows-mode resolve - safe-path/separator-in-segment — encoded path-separator in a segment (URL / fullwidth / overlong UTF-8 / division-slash) - safe-path/ads-marker — NTFS Alternate Data Stream foo:bar marker - safe-path/realpath-escapes-base — symlink resolution escapes base (opt-in via opts.realpath)

Per-segment filename validation composes b.guardFilename's reserved-name + overlong UTF-8 + bidi tables; the multi-segment resolve + base-escape check is the new code.

b.safePath.resolve(base, rel, opts?) #

stable0.10.9
{
  realpath:         boolean,         // default false; true → fs.realpathSync check (symlink-escape)
  platform:         string,          // "windows" forces win-trailing / UNC refusal regardless of host
  allowAbsoluteRel: boolean,         // default false; opt-in for absolute rel that still resolves inside base
}

Resolve rel against base and return the absolute canonicalized path — guaranteed to lie strictly within base. Throws SafePathError with a stable refusal code on any rejection.

var p = b.safePath.resolve("/srv/uploads", req.body.path);
// → "/srv/uploads/"  OR  throws SafePathError on traversal

b.safePath.resolveOrNull(base, rel, opts?) #

stable0.10.9
{
  realpath:         boolean,
  platform:         string,
  allowAbsoluteRel: boolean,
}

Same contract as resolve but returns null on refusal instead of throwing. Useful for hot-path callers that want a boolean-ish gate without try/catch overhead.

var p = b.safePath.resolveOrNull("/srv/uploads", req.body.path);
if (p === null) { res.statusCode = 400; res.end("bad path"); return; }

b.safePath.validate(base, rel, opts?) #

stable0.10.9
{
  realpath:         boolean,
  platform:         string,
  allowAbsoluteRel: boolean,
}

Same gate as resolve but returns a verdict object instead of throwing — { ok: true, resolved } on success, { ok: false, code, message } on refusal. Use when the caller wants to log the refusal class without throw/catch.

var v = b.safePath.validate("/srv/uploads", req.body.path);
if (!v.ok) { res.end("rejected: " + v.code); return; }

b.safePath.confineToBase(base, rel, opts?) #

stable0.17.16
{
  platform: string,   // "windows" forces win32 path semantics regardless of host
}

The lexical traversal-containment core, WITHOUT the user-input strictness of resolve (no reserved-name / ADS / bidi / control-char refusal). Resolve rel against base using the TARGET platform's path semantics and confirm the result stays strictly inside base; return the confined absolute path, or null if it escapes.

This is the barrier resolve layers its user-input checks on top of, and the one a consumer composes when it wants ONLY traversal containment and runs its OWN, separately-calibrated filename validation — as b.staticServe does, keeping its per-file basename gate (b.guardFilename) a distinct step rather than fusing resolve's all-segment user-input strictness into the containment barrier.

var p = b.safePath.confineToBase("/srv/www", "docs/a.html");
// → "/srv/www/docs/a.html"  (null if rel escaped /srv/www)

Last updated 2026-08-08T16:39:49.652Z by seeder.