BTC
ETH
SOL
BNB
GOLD
XRP
DOGE
ADA
Back to home
Security

[HIGH] Security Advisory: @tinacms/graphql’s Media Endpoints Can Escape the Media Root via Symlinks or Junctions (@tinacms/graphql)

TinaCMS's dev media endpoints in @tinacms/cli and @tinacms/graphql suffer from a symlink and junction traversal vulnerability.

TinaCMS’s dev media endpoints in @tinacms/cli and @tinacms/graphql suffer from a symlink and junction traversal vulnerability. Recent lexical path-traversal fixes check only string paths, ignoring filesystem links already under the media root. Attackers can list, write, and delete files outside the root if a symlink or junction points there.

This hits the /media/list, /media/upload, and delete routes on the local dev server. A path like pivot/secret.txt passes validation if pivot is a junction to an external directory, but operations dereference to the target. Verified on Windows with junctions; symlinks work similarly on Unix.

Vulnerability Mechanics

TinaCMS updated its media handlers with these functions:

function resolveWithinBase(userPath: string, baseDir: string): string {
  const resolvedBase = path.resolve(baseDir);
  const resolved = path.resolve(path.join(baseDir, userPath));
  if (resolved === resolvedBase) {
    return resolvedBase;
  }
  if (resolved.startsWith(resolvedBase + path.sep)) {
    return resolved;
  }
  throw new PathTraversalError(userPath);
}

function resolveStrictlyWithinBase(userPath: string, baseDir: string): string {
  const resolvedBase = path.resolve(baseDir) + path.sep;
  const resolved = path.resolve(path.join(baseDir, userPath));
  if (!resolved.startsWith(resolvedBase)) {
    throw new PathTraversalError(userPath);
  }
  return resolved;
}

These rely on Node’s path.resolve, which doesn’t follow symlinks—it resolves lexically. The validated path then feeds fs.readdir, fs.createWriteStream, and fs.remove, which do follow links.

Result: A symlink uploads/pivot -> /etc lets /media/list/pivot/passwd dump /etc/passwd. Writes create files in the target. Deletes wipe them.

Proof of Concept

Repro on Windows (junctions act like symlinks for directories):

Tina validates pivot as inside, lists secret.txt, writes pivot/written-from-media.txt (lands in outside), confirms contents match.

{
  "media": {
    "base": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\public\\uploads",
    "resolvedListPath": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\public\\uploads\\pivot",
    "listedEntries": ["secret.txt"],
    "resolvedWritePath": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\public\\uploads\\pivot\\written-from-media.txt",
    "outsideWriteExists": true,
    "outsideWriteContents": "MEDIA_ESCAPE"
  }
}

Unix symlinks reproduce identically. No auth on dev server, so local network access or shared dev envs expose it.

Why This Matters and Fixes

TinaCMS powers Git-based CMS for Next.js, Gatsby, etc. Dev servers run on localhost:3000 by default, but devs often expose via ngrok, Tailscale, or misbind to 0.0.0.0. An attacker on the LAN—or worse, internet—uploads a symlink, then escapes to read ~/.ssh/id_rsa, env, source code, or npmrc tokens. Writes drop ransomware or backdoors. Deletes nuke repos.

Real-world risk: Dev workflows share previews. Tools like Vercel or Netlify dev proxies amplify if misconfigured. No CVSS yet, but high—arbitrary R/W outside sandbox.

Fixes demand symlink-aware checks. Use fs.realpathSync post-resolution:

const realTarget = fs.realpathSync(resolvedPath);
if (!realTarget.startsWith(realBase + path.sep)) {
  throw new Error('Path outside base after symlink resolution');
}

Or fs.lstatSync to detect/deny links. Canonicalize fully. Tina patched lexical ../ recently—good start, but incomplete. Devs: Audit media roots, block symlinks at upload, run dev servers firewalled.

Update @tinacms/cli ASAP; watch GitHub for patch. This exposes a classic pitfall: path.resolve ≠ secure containment. Test your stacks—symlinks bite everyone eventually.

April 1, 2026 · 3 min · 8 views · Source: GitHub Security

Related