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
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed 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

clean_path() Path Traversal: rel_path Escapes USERDATA Dir

A path traversal flaw in the `clean_path()` helper let a user-controlled `rel_path` value escape the intended USERDATA directory and reach arbitrary files on disk. The function joined path segments without checking the final result, so `../` sequences in route parameters or query strings could be used to read or write files outside the sandboxed storage area. The fix normalizes the path and verifies it still resolves inside the USERDATA root before returning it, raising an error otherwise.

critical

path.resolve() Path Traversal in Node CLI's Dynamic import()

A Node.js CLI script for validating expression definitions took a file path from `process.argv[2]`, resolved it with `path.resolve()`, and passed the result straight into a dynamic `import()` — with no check that the resolved path stayed inside the working directory. An attacker (or a malicious skill/plugin invocation) could supply traversal sequences to load and execute arbitrary `.js` files from anywhere on disk. The fix adds a boundary check with `path.relative()` and an extension allowlist b

high

bookDir() Path Traversal via Unsanitized bookId Parameter

The `bookDir()` function accepted unsanitized `bookId` values derived from user-created book titles, enabling path traversal attacks through `../` sequences. A fix was applied that validates the identifier using `path.basename()` and throws on mismatch, ensuring all resolved paths remain within `LIBRARY_DIR`.

high

Express `app.get('*')` Wildcard Handler Path Traversal in watch.js

A first-party Express server's wildcard route handler used `req.url.indexOf('font.woff2')` to gate access to a font file, allowing attackers to bypass the substring check with crafted paths. The fix replaces the catch-all handler with explicit route registration.

high

updateCardBg() Follows Unvalidated 302 Location Headers

A background-image updater fetched a configured image URL with manual redirect handling and then re-issued the request to whatever `Location` header came back, with no scheme or host checks. A redirect to `http://169.254.169.254/` or `http://127.0.0.1:<port>/` would have been followed with the original fetch options attached, and the response body written to disk as an image asset. The fix resolves the redirect target against `imgDownloadUrl` and rejects anything that is not HTTPS on the same ho

high

markitdown_bridge.py Path Traversal: Arbitrary File Read via sys.argv

The markitdown_bridge.py script, used by MDView for DOCX-to-Markdown conversion, accepted file paths directly from command-line arguments without validating they stayed within intended directories. An attacker could exploit this to read arbitrary files from the filesystem by passing path traversal sequences in the source_path parameter.