CVE-2026-28791 — tinacms
HIGHCVE-2026-28791 is a high-severity (CVSS 7.4) Path Traversal vulnerability in tinacms. A fix is available for tinacms — see the affected versions and patch details below.
Path Traversal in Media Upload Handle in Tina
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-28791.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
CVE-2026-28791 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 378,156 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.
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.7npm install tinacms@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, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update tinacms to 2.1.7 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-28791 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 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-28791 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-28791. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-28791 in your dependencies?
O3 Security finds CVE-2026-28791 across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.