NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
HIGH severity

CVE-2026-28791 — tinacms

HIGH

CVE-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

Also known asGHSA-5hxf-c7j4-279c
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Nov 9, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

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

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk+0.03%
Lower risk than most CVEs32th percentile — riskier than 32% of all scored CVEsHighest risk
0.00%0.32%0.63%0.95%0.1%0.4%May 26Sep 26Nov 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

Where this sits among everything scored

Real-World Exposure

1 pkg affected

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.

32other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
tinacmsnpm
83Kdownloads / week

Description

Affected Package

FieldValue
Package@tinacms/cli
Version2.0.5 (latest at time of discovery)
Vulnerable Filepackages/@tinacms/cli/src/next/commands/dev-command/server/media.ts
Vulnerable Lines42-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:

  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:

#!/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 proxy_pass 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:

// 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/authorized_keys 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:

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

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

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmtinacmsall versions2.1.7npm install tinacms@2.1.7

Affected Products

1 product · 1 configurations
Application
tinacms\/clissw
< 2.1.7
range

Detection & mitigation playbook

Open-source dependency
  1. Detect

    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.

  2. 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.

  3. Workarounds

    Resolve every user-supplied path to its canonical form and reject anything that escapes the intended directory, and run the component under an account that has no read or write access outside the directory it legitimately serves.

Frequently Asked Questions

## 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 t
O3 Security · Impact-Aware SCA

Is CVE-2026-28791 in your dependencies?

Find it across npm, including transitive dependencies.

CVE-2026-28791: tinacms RCE — Fixed in 2.1.7