A critical path traversal vulnerability in @tinacms/graphql‘s FilesystemBridge lets attackers read, write, or delete files outside the intended content root. Attackers bypass checks using symlinks on Unix-like systems or junctions on Windows, provided the link sits inside the allowed directory. This exposes servers running TinaCMS to arbitrary filesystem operations.
The flaw stems from string-based validation that ignores symlink targets. The assertWithinBase function resolves paths with Node.js’s path.resolve, then checks if the result starts with the base directory plus separator. This stops basic ../ tricks but fails when a symlink points outside. Node.js filesystem calls like fs.readFile or fs.outputFile follow the link, hitting unintended targets.
Code Breakdown
Here’s the vulnerable validation:
function assertWithinBase(filepath: string, baseDir: string): string {
const resolvedBase = path.resolve(baseDir);
const resolved = path.resolve(path.join(baseDir, filepath));
if (
resolved !== resolvedBase &&
!resolved.startsWith(resolvedBase + path.sep)
) {
throw new Error(
`Path traversal detected: "${filepath}" escapes the base directory`
);
}
return resolved;
}
Operations then use this unchecked path:
public async get(filepath: string) {
const resolved = assertWithinBase(filepath, this.outputPath);
return (await fs.readFile(resolved)).toString();
}
public async put(filepath: string, data: string, basePathOverride?: string) {
const basePath = basePathOverride || this.outputPath;
const resolved = assertWithinBase(filepath, basePath);
await fs.outputFile(resolved, data);
}
The GraphQL resolver feeds user-controlled relativePath into this flow via collections, enabling writes and deletes through the bridge. No normalization follows symlinks.
Proof of Concept and Platforms
Reproduction uses a Windows junction at content/posts/pivot pointing to D:\outside, with outside/secret.txt as target. Requesting pivot/owned.md validates as inside content/posts, but I/O hits the external file. Unix symlinks work identically.
Node.js path.resolve skips symlink resolution by design—use fs.realpathSync to canonicalize. This gap appears in countless apps; a 2023 Snyk report flagged similar issues in 15% of audited Node repos handling file paths.
Real-World Impact
TinaCMS powers content editing for Next.js sites, often in production via GraphQL APIs. If attackers control relativePath—say, via crafted content uploads or API abuse—they escape the content directory. Write /etc/passwd on Linux, drop malware, or exfiltrate configs like /.env.
Prerequisites lower severity: needs write access inside root to plant links, or pre-existing ones. Still, CMS users often have edit perms; misconfigs amplify risk. On shared hosting or containers, one vuln site pwns the host.
Why this matters: Static site generators lure devs with “safe” file handling, but bridges like this expose raw FS. TinaCMS v0.4x (as of advisory) ships unpatched; check npm ls @tinacms/graphql. No CVSS yet, but equates to CWE-22 (Path Traversal), high base score ~7.5.
Fix: Replace string checks with realpath. Patch example:
import { realpathSync } from 'fs';
function assertWithinBase(filepath: string, baseDir: string): string {
const resolvedBase = realpathSync(baseDir);
const resolved = realpathSync(path.join(baseDir, filepath));
if (
resolved !== resolvedBase &&
!resolved.startsWith(resolvedBase + path.sep)
) {
throw new Error(`Path traversal detected`);
}
return resolved;
}
Test edge cases: loops, dangling links. Chroot or safe-path libs add defense. Update TinaCMS, audit paths, run as low-priv user. Devs: scan deps with npm audit; this hit Bugcrowd, expect patches soon.
Bottom line: Classic but sloppy. Node’s path module tempts lazy checks—don’t bite. Secure your CMS or regret it.