GHSA-5hxf-c7j4-279c
HIGHGHSA-5hxf-c7j4-279c is a high-severity (CVSS 7.4) Path Traversal vulnerability in tinacms. O3 Security confirms whether GHSA-5hxf-c7j4-279c is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Tina: Path Traversal in Media Upload Handle
Real-World Exposure
How broadly this vulnerability is actually deployed: weekly install volume shows current usage, a proxy for how much of the ecosystem is exposed.
tinacmsnpmDescription
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
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:
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:
- Delete Handler (
handleDelete, lines 29-33) - Arbitrary file deletion - List Handler (
handleList, lines 16-27) +MediaModel.listMedia- Directory enumeration - MediaModel.deleteMedia (lines 201-217) - Arbitrary file deletion
Similar code also exists in the Express version at:
packages/@tinacms/cli/src/server/routes/index.tspackages/@tinacms/cli/src/server/models/media.ts
PoC
Quick Verification (No Server Required)
This Node.js script directly tests the vulnerable code logic:
#!/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.txtis mitigated in the default configuration
Why this is still a valid vulnerability that should be fixed:
- The code itself has no validation - If the path reaches the handler (via any vector), it will be exploited
- Defense-in-depth principle - Security should not rely solely on HTTP normalization
- Inconsistent protection - Your GraphQL layer (
addPendingDocument) explicitly validates paths and rejects../(see test atpackages/@tinacms/graphql/tests/pending-document-validation/index.test.ts:59), but the media endpoints don't have equivalent protection - Different deployment contexts:
- Reverse proxies (nginx, Apache) with
proxy_passmay preserve raw paths - Custom server configurations
- Future refactoring that uses this code differently
- Reverse proxies (nginx, Apache) with
- The
parseMediaFolderhelper (line 66-74) shows intent to restrict paths - the upload handler should have similar restrictions - Express version also affected -
packages/@tinacms/cli/src/server/routes/index.tshas the same pattern
Evidence That Path Traversal Should Be Blocked
Your codebase already shows that path traversal is considered a security issue:
// 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
-
Remote Code Execution: Write malicious files to executable locations
- Overwrite
~/.ssh/authorized_keysfor SSH access - Modify application source code
- Create cron jobs or systemd services
- Overwrite
-
Denial of Service: Delete critical application or system files
-
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:
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:
handleDeletefunctionhandleListfunctionMediaModel.listMediamethodMediaModel.deleteMediamethod- Express router in
packages/@tinacms/cli/src/server/
Alternative: Create a Validation Helper
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
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | tinacms | all versions | 2.1.7 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for tinacms. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update tinacms to 2.1.7 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-5hxf-c7j4-279c is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-5hxf-c7j4-279c is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-5hxf-c7j4-279c. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-5hxf-c7j4-279c in your dependencies?
O3 detects GHSA-5hxf-c7j4-279c across npm dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.