Back to Blog
critical SEVERITY7 min read

How Path Traversal happens in Node.js agents and how to fix it

A path traversal vulnerability in `skills/baoyu-design/agents/import-design-system.mjs` allowed attackers to escape the intended design system directory by supplying absolute paths, bypassing a guard that only checked for `..` prefixes. The fix adds an `isAbsolute()` check alongside the existing relative-path guard, closing the bypass with a single targeted change. This matters because the `dsDir` argument is user-controlled, meaning any caller of the script could redirect file operations to sen

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is a path traversal vulnerability (CWE-22) in a Node.js `.mjs` agent (`import-design-system.mjs`). The `cssAssetTargets` function only checked whether normalized paths started with `..`, but an attacker could bypass this guard by supplying an absolute path (e.g., `/etc/passwd`). The fix adds `path.posix.isAbsolute(relToDs)` to the guard condition, so both relative escapes and absolute paths are rejected. Any Node.js code that normalizes user-supplied paths must check for both relative traversal sequences and absolute path injection.

Vulnerability at a Glance

cweCWE-22
fixAdded `path.posix.isAbsolute(relToDs)` to the skip condition on line 96
riskAttacker reads or overwrites arbitrary files on the server by controlling the design system source directory
languageJavaScript (Node.js / ESM)
root causePath guard in `cssAssetTargets` only rejected `..`-prefixed paths, not absolute paths
vulnerabilityPath Traversal

The File That Imports Design Systems — and Almost Exported Sensitive Files

The skills/baoyu-design/agents/import-design-system.mjs file is responsible for pulling CSS assets from a design system directory and copying them into a project. It's the kind of utility script that lives quietly in a monorepo, rarely reviewed for security because it "just moves files around." But a subtle flaw in its path-validation logic meant that an attacker who could control the dsDir argument — or craft a malicious CSS asset reference — could point the script at any directory on the filesystem.

Orbis AppSec's multi-agent AI scanner flagged this as a high-severity path traversal (CWE-22) and automatically opened a pull request with a one-line fix. Here's exactly what went wrong, how it could be exploited, and what the fix does.


The Vulnerability Explained

What cssAssetTargets Does

The cssAssetTargets function (around line 93) parses CSS files for referenced assets — fonts, images, icons — and returns the relative paths that need to be copied alongside the CSS. The logic looks roughly like this:

// VULNERABLE — before the fix
function cssAssetTargets(cssRel) {
  // ...
  for (const u of urls) {
    u = u.replace(/[?#].*$/, ''); // strip ?v=… / #iefix
    if (!u) continue;
    const relToDs = path.posix.normalize(path.posix.join(dir, u));
    if (relToDs.startsWith('..')) continue; // outside the DS folder — skip
    out.push(relToDs);
  }
  return out;
}

The intent is clear: normalize the joined path and skip anything that escapes the design system folder via ... This is a reasonable first instinct, but it only covers one of the two ways a path can escape its sandbox.

The Bypass: Absolute Paths

path.posix.normalize has a well-known behavior — it does not strip a leading /. If a URL reference inside a CSS file resolves to an absolute path after normalization (e.g., /etc/fonts/custom.ttf, or if dir itself is absolute and u is crafted to stay absolute), the startsWith('..') guard is completely silent. The path does not start with .., so it passes the check and lands in out.

The second, more critical vector is the dsDir argument itself. The PR description confirms it: dsDir is user-controlled without validation. If an attacker can invoke the script with dsDir=/etc or dsDir=/home/user/.ssh, the entire directory traversal logic operates on a sensitive base directory from the start.

Concrete Attack Scenario

Imagine a CI/CD pipeline or a developer tool that calls:

node import-design-system.mjs --dsDir /path/to/design-system

If the --dsDir flag is sourced from user input (a config file, a UI field, an API parameter), an attacker could substitute:

node import-design-system.mjs --dsDir /etc

Or, within a CSS file that the script processes, include a reference like:

@font-face {
  src: url('/etc/passwd');
}

After path.posix.normalize(path.posix.join(dir, '/etc/passwd')), the result is /etc/passwd — an absolute path that does not start with .. — so the old guard lets it through. The script then attempts to read and copy /etc/passwd as a font asset.

Real-World Impact

This is a Node.js library used in a design tooling context. Downstream consumers who automate design system imports — especially in server-side or CI environments — are at risk of:

  • Arbitrary file read: sensitive configuration files, SSH keys, environment files
  • Potential file overwrite: depending on how the copy destination is constructed
  • Supply-chain risk: if the design system source is fetched from an external or untrusted location, malicious CSS could trigger the traversal automatically

The Fix

The fix is surgical: a single additional condition added to the existing guard on line 96.

Before

if (relToDs.startsWith('..')) continue; // outside the DS folder — skip

After

if (relToDs.startsWith('..') || path.posix.isAbsolute(relToDs)) continue; // outside the DS folder — skip

What changed: path.posix.isAbsolute(relToDs) returns true for any path that begins with /. By short-circuiting on this condition, the function now rejects:

  1. Paths that escape the sandbox via relative traversal (../../../etc/passwd)
  2. Paths that are absolute from the start (/etc/passwd, /var/secrets/key)

The comment is preserved and extended implicitly — the guard now correctly enforces "outside the DS folder" for both escape vectors.

Why This Is the Right Fix

The fix operates on the already-normalized value of relToDs, so there's no risk of encoding tricks slipping through before the check. path.posix.isAbsolute is a stable, well-tested Node.js built-in with no edge cases around URL encoding or platform differences (since path.posix is used explicitly, not path — ensuring consistent /-based behavior regardless of the host OS).


Key Takeaways

  • Checking startsWith('..') alone is not a complete path traversal guard — absolute paths bypass it entirely, as demonstrated in cssAssetTargets.
  • User-controlled root arguments (dsDir) are high-value attack targets — validate them against an allowlist or a resolved base directory before any file operations.
  • path.posix.isAbsolute() is the missing half of the guard — always pair relative-escape checks with an absolute-path check when working with path.posix.
  • Design tooling scripts are not exempt from security review — utilities that "just move files" can read or overwrite sensitive data if their path logic is flawed.
  • The fix was one line — security improvements don't have to be complex; the key is knowing which check is missing.

How Orbis AppSec Detected This

  • Source: The dsDir argument passed to the import-design-system.mjs agent, and URL values parsed from CSS files processed by cssAssetTargets
  • Sink: path.posix.normalize(path.posix.join(dir, u)) at line 96 of skills/baoyu-design/agents/import-design-system.mjs, whose result is pushed into the out array and used in subsequent file-copy operations
  • Missing control: No check for absolute paths after normalization — path.posix.isAbsolute(relToDs) was absent from the guard condition
  • CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
  • Fix: Added || path.posix.isAbsolute(relToDs) to the existing skip condition, ensuring absolute paths are rejected alongside relative traversal sequences

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

Path traversal vulnerabilities are deceptively easy to introduce and deceptively easy to under-fix. The guard in cssAssetTargets was well-intentioned — it blocked the most obvious ..-based escape — but missed the equally dangerous absolute-path vector. The lesson from this specific fix is that path confinement requires checking all escape routes, not just the most familiar one.

If your codebase has any utility that joins a user-controlled string with path.join or path.posix.join, take five minutes to audit its guard. Ask: does this check block .. sequences? Does it also block absolute paths? Does it validate the root argument itself? If the answer to any of those is "no," you may have a CWE-22 waiting to be found.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #15

Related Articles

high

modelExporter.js Path Traversal via Unsanitized Directory Concatenation

A path traversal vulnerability in `modelExporter.js` allowed attackers to read arbitrary files by injecting traversal sequences into directory and relative path parameters. The `readSourceFile` function concatenated these unsanitized inputs directly into file URLs passed to `fetch()`. The fix introduces strict path normalization that rejects attempts to escape the intended directory.

critical

How path traversal happens in PHP virtual filesystem adapters and how to fix it

A critical path traversal flaw in `VirtualAdapter.php`'s `resolveMount()` method allowed attackers to escape mounted directory boundaries using sequences like `../../../etc/passwd`. The fix introduces `PathPolicy::normalizeRelative()` to sanitize the remaining path segment before it ever reaches the underlying storage adapter.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

high

How Trust-Prefix Bypass via Path Traversal Happens in Python Copier and How to Fix It

CVE-2026-53951 is a high-severity path traversal vulnerability in Copier 9.15.0 that allowed attackers to bypass trust-prefix checks and execute tasks without user confirmation. Upgrading to Copier 9.15.2 eliminates this attack vector by properly validating file paths before task execution.

critical

How Path Traversal in basic-ftp Leads to File Overwrite Attacks and How to Fix It

CVE-2026-27699 is a critical path traversal vulnerability in basic-ftp versions before 5.3.1 that allows attackers to overwrite arbitrary files on the system by crafting malicious file paths. This vulnerability was fixed by upgrading basic-ftp and enforcing strict version constraints across dependent packages. Understanding this attack and its mitigation is essential for developers using FTP libraries in production environments.

critical

How Command Injection Vulnerabilities Happen in Python Subprocess Calls and How to Fix Them

A critical command injection vulnerability was discovered in `src/unused/server/fft.py` where external binaries like `oggenc` and `cocoa_text` were executed with file path parameters that could be manipulated by user input. Although `shell=False` was used, the lack of input validation allowed attackers to potentially trigger processing of arbitrary files or cause denial of service. This fix implements proper path validation to prevent exploitation.