Listen to this Post
How the Vulnerability Works
The vulnerability exists in `@tinacms/graphql` due to a realpath gap in its filesystem containment checks. The `FilesystemBridge` validates paths using a string-based `startsWith` comparison after resolving lexical path components with path.resolve(). This blocks simple `../` traversal but fails to consider symbolic links or Windows junctions.
When a symlink or junction is present inside the allowed content root (e.g., a collection path like `content/posts` pointing to outside/), a user-controlled `relativePath` such as `pivot/owned.md` is passed through the validation. The `assertWithinBase()` function computes:
resolved = path.resolve(path.join(baseDir, filepath))
Because the link target is not resolved, the resulting path string appears to reside under baseDir. The check passes, and the file operation proceeds with the lexical path.
The actual filesystem I/O (via fs.readFile, fs.outputFile, fs.remove) then follows the symlink/junction, causing reads, writes, or deletions on files outside the intended root. This bypasses all prior path-traversal fixes that only blocked `..` patterns.
The issue is reachable through Tina’s GraphQL resolvers, where `relativePath` is taken from user input, validated only with string methods, and then passed to bridge.put()/delete()/get(). The observed result with a junction shows successful read of `secret.txt` and write of `owned.md` outside the root.
DailyCVE Form
Platform: TinaCMS GraphQL
Version: All prior
Vulnerability: Symlink path traversal
Severity: Critical
date: 2026-03-31
Prediction: Patch soon
What Undercode Say:
Simulate the realpath gap ln -s /outside /allowed/content/posts/pivot Request relativePath = "pivot/owned.md" Validation passes; read/write hits /outside/owned.md
// Vulnerable validation snippet
function assertWithinBase(filepath, baseDir) {
const resolvedBase = path.resolve(baseDir);
const resolved = path.resolve(path.join(baseDir, filepath));
if (!resolved.startsWith(resolvedBase + path.sep)) {
throw new Error("Path traversal detected");
}
return resolved;
}
How Exploit:
- Ensure a symlink/junction exists under a writable collection path (e.g., `content/posts/pivot` pointing to
../../outside). - Send a GraphQL mutation with `relativePath` pointing into the symlink (e.g.,
pivot/owned.md). - The server validates the path lexically, passes the check.
- Filesystem operation follows the symlink and reads/writes outside the intended root.
Protection from this CVE
- Replace string-based checks with canonical path comparison using
fs.realpath(). - Resolve the base and the candidate path’s parent to real paths before allowing I/O.
- For write operations, canonicalize the nearest existing parent directory.
- Do not rely solely on
path.resolve().startsWith().
Impact
- Arbitrary file read outside the content root.
- Arbitrary file write/deletion outside the content root.
- Breaks security assumptions of prior path-traversal fixes.
- Exploitable if an attacker can place a symlink/junction inside the content tree (e.g., via commit or prior write).
🎯Let’s Practice Exploiting & Learn Patching For Free:
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

