# @tinacms/graphql's `FilesystemBridge` Path Validation Can Be Bypassed via Symlinks or Junctions

TinaCMS FilesystemBridge validates paths lexically, so a symlink/junction inside the content root lets GraphQL content ops read, write and delete files outside the root.

- Payout: Research
- Severity: high
- Program: TinaCMS
- Found by: offset
- Disclosed: 2026-04-01
- CVE: CVE-2026-34604
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-g9c2-gf25-3x67
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-g9c2-gf25-3x67
- Tags: area:web3 domain:oss impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:graphql tech:nodejs technique:source-review vuln:path-traversal

**Target:** FilesystemBridge in @tinacms/graphql (GraphQL content/database layer)
**Root cause:** assertWithinBase compares path.resolve(...).startsWith(resolvedBase + sep) on the lexical string only; it never canonicalizes symlink/junction targets, so a link inside the base resolves to an out-of-root file at I/O time.
**Steps:**
1. Create content root with allowed collection `content/posts`.
2. Place a junction/symlink inside the collection: `content/posts/pivot -> <root>/outside`.
3. Put `outside/secret.txt` outside the content root.
4. Issue a GraphQL/content operation with `relativePath` `pivot/owned.md`; resolver builds `content/posts/pivot/owned.md` and validatePath accepts it.
5. Bridge `get()` reads `outside/secret.txt`; bridge `put()` writes `outside/owned.md`.
**How found:** Noticed the containment check was purely string-based (path.resolve + startsWith) while the bridge performs real fs I/O, hypothesizing that link resolution at I/O time diverges from the lexical check; verified with a Windows junction repro.
**Exploitation:** Requires a symlink/junction already present under the content tree (committed to repo or attacker-planted). Then GraphQL content read/write/delete operations with a relativePath traversing the link perform filesystem I/O on targets outside the configured root, yielding arbitrary file read/write and potential delete.
**Insight:** When a path guard uses path.resolve/startsWith, check whether any sink later follows links: plant a symlink/junction inside the allowed root and see if the lexical check still passes while fs.readFile/writeFile lands outside.

