Where
-Infinity
0
Severity
8.8
Path Traversal
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:L

Summary

@tinacms/graphql uses string-based path containment checks in FilesystemBridge:

- path.resolve(path.join(baseDir, filepath)) - startsWith(resolvedBase + path.sep)

That blocks plain ../ traversal, but it does not resolve symlink or junction targets. If a symlink/junction already exists under the allowed content root, a path like content/posts/pivot/owned.md is still considered "inside" the base even though the real filesystem target can be outside it.

As a result, FilesystemBridge.get(), put(), delete(), and glob() can operate on files outside the intended root.

Details

The current bridge validation is:

ts 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; }

But the bridge then performs real filesystem I/O on the resulting path:

ts 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); }

public async delete(filepath: string) { const resolved = assertWithinBase(filepath, this.outputPath); await fs.remove(resolved); }

This is a classic realpath gap:

1. validation checks the lexical path string 2. the filesystem follows the link target during I/O 3. the actual target can be outside the intended root

This is reachable from Tina's GraphQL/local database flow. The resolver builds a validated path from user-controlled relativePath, but that validation is also string-based:

ts const realPath = path.join(collection.path, relativePath); this.validatePath(realPath, collection, relativePath);

Database write and delete operations then call the bridge:

ts await this.bridge.put(normalizedPath, stringifiedFile); ... await this.bridge.delete(normalizedPath);

Local Reproduction

This was verified llocally with a real junction on Windows, which exercises the same failure mode as a symlink on Unix-like systems.

Test layout:

- content root: D:\bugcrowd\tinacms\temp\junction-repro4 - allowed collection path: content/posts - junction inside collection: content/posts/pivot -> D:\bugcrowd\tinacms\temp\junction-repro4\outside - file outside content root: outside\secret.txt

Tina's current path-validation logic was applied and used to perform bridge-style read/write operations through the junction.

Observed result:

json { "graphqlBridge": { "collectionPath": "content/posts", "requestedRelativePath": "pivot/owned.md", "validatedRealPath": "content\\posts\\pivot\\owned.md", "bridgeResolvedPath": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\content\\posts\\pivot\\owned.md", "bridgeRead": "TOPSECRETFROMOUTSIDE\\r\\n", "outsideGraphqlWriteExists": true, "outsideGraphqlWriteContents": "GRAPHQLESCAPE" } }

That is the critical point:

- the path was accepted as inside content/posts - the bridge read outside\secret.txt - the bridge wrote outside\owned.md

So the current containment check does not actually constrain filesystem access to the configured content root once a link exists inside that tree.

Impact

- Arbitrary file read/write outside the configured content root - Potential delete outside the configured content root via the same assertWithinBase() gap in delete() - Breaks the assumptions of the recent path-traversal fixes because only lexical traversal is blocked - Practical attack chains where the content tree contains a committed symlink/junction, or an attacker can cause one to exist before issuing GraphQL/content operations

The exact network exploitability depends on how the application exposes Tina's GraphQL/content operations, but the underlying bridge bug is real and independently security-relevant.

Recommended Fix

The containment check needs to compare canonical filesystem paths, not just string-normalized paths.

For example:

1. resolve the base with fs.realpath() 2. resolve the candidate path's parent with fs.realpath() 3. reject any request whose real target path escapes the real base 4. for write operations, carefully canonicalize the nearest existing parent directory before creating the final file

In short: use realpath-aware containment checks for every filesystem sink, not path.resolve(...).startsWith(...) alone.

Resources

- packages/@tinacms/graphql/src/database/bridge/filesystem.ts - packages/@tinacms/graphql/src/database/index.ts - packages/@tinacms/graphql/src/resolver/index.ts

1 / 2
Source: GitHub
First published (updated )
Severity
8.1
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

Summary A Path Traversal vulnerability in @tinacms/graphql allows unauthenticated users to write and overwrite arbitrary files within the project root. This is achieved by manipulating the relativePath parameter in GraphQL mutations. The impact includes the ability to replace critical server configuration files and potentially execute arbitrary commands by sabotaging build scripts.

Details The vulnerability exists in the path validation logic within @tinacms/graphql. Specifically, the regex-based validation in getValidatedPath fails to recognize backslashes (\) as directory separators on non-Windows platforms (Mac/Linux). An attacker can provide a path like x\..\..\..\package.json, which bypasses the validation check but is subsequently treated as a traversal path during file I/O operations by the underlying fs modules and path normalization utilities.

Incriminated code areas: - packages/@tinacms/graphql/src/database/bridge/filesystem.ts: assertWithinBase function. - packages/@tinacms/graphql/src/resolver/index.ts: getValidatedPath function.

PoC 1. Start the TinaCMS development server. 2. Send a malicious GraphQL mutation to overwrite a project file (e.g., package.json):

bash curl -X POST http://localhost:4001/graphql \ -H "Content-Type: application/json" \ -d '{"query": "mutation { updateDocument(collection: \"global\", relativePath: \"x\\\\..\\\\..\\\\..\\\\package.json\", params: { global: { header: { name: \"OVERWRITTEN\" } } }) { typename } }"}'

3. Observe that the root package.json has been replaced with the provided payload.

<img width="1424" height="516" alt="2026-03-1512-24-05 PM" src="https://github.com/user-attachments/assets/9fdf94ce-2183-4a24-9cd9-48f21deb9768" />

<img width="1387" height="774" alt="2026-03-1512-27-33 PM" src="https://github.com/user-attachments/assets/676f083b-f934-4cf2-978b-bb2fabee0216" />

Impact This is an Arbitrary File Write vulnerability. Any unauthenticated user with network access to the GraphQL API can: - Overwrite critical server configuration files (e.g., package.json, tsconfig.json). - Host malicious scripts in the public/ directory for client-side attacks. - Perform Arbitrary Code Execution by modifying build scripts or server-side logic files that are subsequently executed by the environment.

Weaknesses: - CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') - CWE-73: External Control of File Name or Path

1 / 2
Source: GitHub
First published (updated )
Severity
6.3
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L

Description

TinaCMS allows users to create, update, and delete content documents using relative file paths (relativePath, newRelativePath) via GraphQL mutations. Under certain conditions, these paths are combined with the collection path using path.join() without validating that the resolved path remains within the collection root directory.

Because path.join() does not prevent directory traversal, paths containing ../ sequences can escape the intended directory boundary.

Attack Vectors

1. File Creation: Create files outside the collection directory graphql createDocument( collection: "post" relativePath: "../../config/malicious.md" params: { post: { title: "malicious" } } )

2. File Move/Rename: Move existing files outside the collection graphql updateDocument( collection: "post" relativePath: "existing.md" params: { relativePath: "../../stolen.md" } )

3. File Deletion: Delete files outside the collection graphql deleteDocument( collection: "post" relativePath: "../../important-config.md" )

4. Folder Creation: Create folders outside the collection graphql createFolder( collection: "post" relativePath: "../../malicious-folder" )

Impact

An authenticated user with document mutation permissions can:

- Create content files outside collection boundaries (subject to schema validation) - Move or rename files outside collection boundaries - Delete content files outside collection boundaries - Read file contents via document retrieval mutations

Mitigating Factors

Several constraints limit the practical impact of this vulnerability:

1. Schema Validation: Created/updated content must conform to the collection's GraphQL schema. Attackers cannot write arbitrary file content—the params argument is validated against the generated mutation types (e.g., PostMutation).

2. Authentication Required: Exploitation requires authenticated access with CMS editor permissions. Anonymous users cannot access GraphQL mutations.

3. Git Tracking: In typical deployments, all file operations are tracked in git (either via GitHub API for Tina Cloud/self-hosted with GitProvider, or local filesystem changes). Malicious changes are visible in version control and can be reverted.

What This Vulnerability Does NOT Allow

- Writing arbitrary file content (content is schema-validated) - Silent/untracked file modifications (changes appear in git) - Unauthenticated access

Proof of Concept

See packages/@tinacms/graphql/tests/path-traversal-security/index.test.ts for automated tests demonstrating the vulnerability.

Manual reproduction: bash node -e " const path = require('path');

const collectionPath = 'content/posts'; const maliciousRelativePath = '../../OUTSIDE/poc.md';

const realPath = path.join(collectionPath, maliciousRelativePath); console.log('Resolved path:', realPath); // Output: OUTSIDE/poc.md (escaped content/posts) "

1 / 2
Source: GitHub
First published (updated )
Severity
7.3
Code Injection
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary tinacms uses the gray-matter package in an insecure way allowing attackers that can control the content of the processed markdown files, e.g., blog posts, to execute arbitrary code.

Details The gray-matter package executes by default the code in the markdown file's front matter. tinacms does not change this behavior when process markdown file, e.g., by passing a custom engine property for js/javascript in the options object.

PoC 1. Create a tinacms app using the cli/documentation: npx create-tina-app@latest 2. Modify one of the blog posts to contain the following front matter: js ---js { "title": "Pawned" + console.log(require("fs").readFileSync("/etc/passwd").toString()) } --- 3. Start the tinacms server, e.g., with npm run dev 4. Observe the console of the server printing the password file, showing that attackers can execute arbitrary commands.

Impact RCE: attackers can execute arbitrary JavaScript code on the server hosting tinacms.

Feasibility Potential attack scenarios can be executed like this: Companies often have technical writers as contractors. These contractors produce md files, which they send over email or upload in a shared cloud folder. Developers download these files and upload them in tinacms's content folder. While this example might appear speculative or contrived, a general observation is that developers would be very surprised to find out that processing untrusted markdown files via tinacms = server-side code execution = complete machine take over. That is, tinacms users might not expect markdown files to contain anything else than data and gray-matter violates that assumption.

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203