← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-28793 · CWE-22 · Disclosed 2026-03-12

Tina is a headless content management system

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is a loaded nail gun left on the workbench, not a sniper rifle pointed at your perimeter

CVE-2026-28793 is a path traversal flaw in the @tinacms/cli development server used by tinacms dev. The vulnerable media endpoints (/media/list/*, /media/upload/*, /media/*) let an attacker read, write, or delete files outside the intended media directory by supplying crafted path segments. Authoritative text from NVD and the GitHub advisory says versions *prior to 2.1.8* are affected; the GHSA metadata also shows <= 2.1.15, which appears inconsistent with the advisory text, so the safe interpretation is to treat anything below 2.1.8 as exposed until your package inventory proves otherwise.

The vendor's HIGH 8.4 is technically understandable because the impact on the affected host is real: arbitrary file access on a developer machine can expose .env files, SSH keys, and source trees, and can poison builds or repos. But in enterprise reality this is mostly a *developer-workstation / cloud-IDE* problem, not a mass internet-exposed server problem: the service is a dev server, binds to localhost by default, is only present while developers are actively running it, and has no evidence of in-the-wild exploitation or KEV inclusion. That combination drags this down to MEDIUM for fleet patch prioritization.

"Serious on a developer box, but this is not a broad internet-facing enterprise fire drill"
02 · The Attack Path

4 steps from start to impact.

STEP 01

Reach a live Tina dev server

The attacker first needs a path to the tinacms dev HTTP listener on port 4001, typically with curl or a simple browser/client. For *this* CVE that usually means a local foothold on the workstation, a forwarded cloud-IDE port, a Docker/VM port map, or a misconfigured bind to 0.0.0.0 as described in the GHSA and NVD references.
Conditions required:
  • A developer is actively running tinacms dev
  • The attacker can reach the HTTP listener locally or through a forwarded/exposed path
  • The installed @tinacms/cli version is below 2.1.8
Where this breaks in practice:
  • Default bind is localhost, which kills broad unauthenticated internet reachability
  • The server is ephemeral and usually only up during active development
  • Only organizations with Tina developers even have this exposure class
Detection/coverage: External scanners often miss this because the service is short-lived and frequently localhost-bound. SCA/SBOM tooling is the reliable coverage layer.
STEP 02

Enumerate files with traversal

Using curl --path-as-is or equivalent, the attacker can hit /media/list/../../../... to walk outside the media folder and enumerate directories. The GHSA includes a concrete curl read example against /media/list/../../../etc/passwd, showing the traversal primitive is straightforward once the listener is reachable.
Conditions required:
  • HTTP access to the dev server
  • Traversal sequences are preserved by the client or proxy
Where this breaks in practice:
  • Some intermediary tooling normalizes paths and breaks traversal
  • Enumeration only matters if valuable files or paths exist on the host
Detection/coverage: Local app logging may be sparse. EDR or proxy telemetry can catch repeated requests containing ../ or unusual access to port 4001.
STEP 03

Write or delete arbitrary files

The attacker then uses the vulnerable upload and delete handlers with crafted paths to overwrite or remove files outside the media root. The vendor advisory demonstrates both POST /media/upload/... for arbitrary writes and DELETE /media/... for arbitrary deletes, again with curl examples.
Conditions required:
  • The target path is writable or deletable by the Tina process user
  • The dev server process has filesystem permissions on the target location
Where this breaks in practice:
  • OS permissions can block writes to sensitive directories
  • Not every write leads to code execution; many only cause corruption or data loss
Detection/coverage: EDR, filesystem auditing, or FIM can catch unexpected file modifications initiated by node or the Tina dev process.
STEP 04

Turn file access into useful impact

Real attacker value comes from reading secrets like .env, SSH keys, cloud credentials, or by overwriting watched source files, scripts, or config that later get executed by the developer toolchain. That can enable source theft, credential theft, or an indirect code-execution path on the workstation, but that last step depends heavily on the local development workflow.
Conditions required:
  • High-value secrets or executable assets are present on the developer host
  • The developer workflow trusts local scripts, builds, or auto-reload behavior
Where this breaks in practice:
  • Impact is usually limited to the single developer box or repo in reach
  • A second-stage execution path is environment-specific, not guaranteed
Detection/coverage: Secret-access anomalies, suspicious git diffs, shell history, and EDR child-process telemetry are the best places to look.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo confirmed exploitation surfaced in the reviewed primary sources. It is not listed in the CISA KEV catalog.
Proof-of-concept availabilityYes. The vendor's GitHub advisory publishes working curl PoC examples for arbitrary read, write, and delete.
EPSS0.00034 (~0.034%) from the user-provided intel — extremely low modeled exploitation probability. I did not directly verify the percentile during this review.
KEV statusNot KEV-listed in the reviewed CISA Known Exploited Vulnerabilities Catalog. No CISA due date applies.
CVSS vector reality checkCVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H means the score already admits this is not a generic remote perimeter bug. AV:L is the whole story: attacker position is the main brake.
Affected versionsAuthoritative NVD/GHSA narrative says prior to 2.1.8 in @tinacms/cli. The GHSA metadata also shows <= 2.1.15, which looks internally inconsistent; treat the textual fix boundary at 2.1.8 as authoritative unless the vendor clarifies otherwise.
Fixed version@tinacms/cli@2.1.8 is the published fix release. The release notes also show related hardening around CORS and Vite filesystem restrictions.
Exposure populationNarrow. The vulnerable component is a development server on default port 4001, and the GHSA says it binds to localhost by default. Real exposure mostly comes from cloud IDEs, port forwarding, containers/VMs, or bad binds.
Scanning and detection coverageSCA/SBOM and dependency scanners should catch this well. Internet ASM and vuln scanning will underperform because these dev servers are often short-lived, local-only, and absent from production inventories.
Disclosure and creditPublished 2026-03-12 in GitHub's advisory flow. Reporter credited by the GHSA: alaeddine03.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to MEDIUM (5.6/10)

The decisive factor is attacker position: this bug lives on a localhost-bound dev server and usually requires either a local foothold or a specially exposed development environment to matter. That sharply limits reachable population, even though successful exploitation on one developer workstation can be ugly.

HIGH Technical impact on an affected host
HIGH Default exposure being materially narrower than a normal internet-facing web flaw
MEDIUM Exact affected version boundary because GHSA metadata and advisory text are inconsistent

Why this verdict

  • Downgrade for attacker position: AV:L is not cosmetic here. To exploit this in the real world, the attacker usually needs a local foothold, a port-forwarded cloud IDE, or a misconfigured dev bind — each one cuts reachable population hard.
  • Downgrade for exposure fraction: this is a *development* server, not a production Tina service. Most enterprise hosts will never run tinacms dev, and most that do are only live during active developer sessions.
  • Keep it at MEDIUM because host impact is real: once reached, the attacker gets arbitrary file read/write/delete against the developer box, which can expose SSH keys, .env secrets, source code, and build scripts.

Why not higher?

There is no evidence of active exploitation, no KEV listing, and the EPSS is tiny. More importantly, the attack chain is bottlenecked by reachability to an ephemeral local dev server, so this does not deserve the same queue position as remotely reachable server-side RCE or auth bypass flaws.

Why not lower?

This is not harmless developer lint. Arbitrary file access on engineering workstations can leak credentials, taint source, and create supply-chain risk if modified scripts or configs get committed or executed later. The single-host blast radius is limited, but the *value* of that single host can be very high.

05 · Compensating Control

What to do — in priority order.

  1. Upgrade @tinacms/cli — Move all repos, base images, and developer environments to 2.1.8 or later. Because this is MEDIUM, there is no noisgate mitigation SLA — go straight to the 365-day remediation window, but prioritize shared dev images and high-value engineering teams first.
  2. Kill unnecessary exposure paths — Stop publishing Tina dev servers through port-forwarding, cloud IDE sharing links, or 0.0.0.0 binds unless there is a documented need. Do this immediately as hygiene even though there is no formal mitigation SLA for a MEDIUM finding.
  3. Watch developer hosts for /media/ abuse — Add temporary detections for suspicious ../ requests to port 4001, unexpected node-initiated file writes, and unusual access to .env, SSH, or repo files. Keep that telemetry in place until the affected developer fleet is remediated within the 365-day window.
  4. Harden cloud IDE defaults — If you use Codespaces, Gitpod, or similar platforms, review default port visibility and sharing rules so localhost-only tooling stays private by default. Fold this into the normal platform hardening cycle and complete it well before the remediation window closes.
What doesn't work
  • A perimeter WAF does little here because the primary exposure is a local or forwarded dev listener, not your production web tier.
  • MFA/SSO does not help because these media endpoints are unauthenticated within the dev server itself.
  • Production-only external vuln scanning will miss a lot of this because the service is ephemeral and often bound only to localhost.
06 · Verification

Crowdsourced verification payload.

Run this on the developer workstation, CI runner, or image build context that contains the repo or installed dependency tree. Invoke it as python3 check_cve_2026_28793.py /path/to/project; it only needs read access to the project files and node_modules, no admin rights.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# CVE-2026-28793 verifier for @tinacms/cli
# Usage: python3 check_cve_2026_28793.py /path/to/project
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

import json
import os
import sys
from typing import List, Optional, Tuple

FIXED = (2, 1, 8)
TARGET = '@tinacms/cli'


def parse_version(v: str) -> Optional[Tuple[int, int, int]]:
    if not v:
        return None
    v = v.strip()
    if v.startswith('v'):
        v = v[1:]
    main = v.split('-')[0].split('+')[0]
    parts = main.split('.')
    if len(parts) < 3:
        return None
    try:
        return (int(parts[0]), int(parts[1]), int(parts[2]))
    except ValueError:
        return None


def is_vulnerable(v: str) -> Optional[bool]:
    pv = parse_version(v)
    if pv is None:
        return None
    return pv < FIXED


def walk_package_lock_deps(deps, hits: List[str]):
    if not isinstance(deps, dict):
        return
    for name, meta in deps.items():
        if name == TARGET and isinstance(meta, dict) and 'version' in meta:
            hits.append(str(meta['version']))
        if isinstance(meta, dict):
            walk_package_lock_deps(meta.get('dependencies', {}), hits)


def read_package_lock(path: str) -> List[str]:
    hits = []
    try:
        with open(path, 'r', encoding='utf-8') as f:
            data = json.load(f)
    except Exception:
        return hits

    packages = data.get('packages')
    if isinstance(packages, dict):
        key = 'node_modules/@tinacms/cli'
        if key in packages and isinstance(packages[key], dict) and 'version' in packages[key]:
            hits.append(str(packages[key]['version']))

    walk_package_lock_deps(data.get('dependencies', {}), hits)
    return hits


def read_installed_package_json(path: str) -> Optional[str]:
    try:
        with open(path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        return data.get('version')
    except Exception:
        return None


def find_versions(root: str) -> List[Tuple[str, str]]:
    findings = []

    # Direct installed package
    pkg_json = os.path.join(root, 'node_modules', '@tinacms', 'cli', 'package.json')
    if os.path.isfile(pkg_json):
        v = read_installed_package_json(pkg_json)
        if v:
            findings.append((pkg_json, v))

    # package-lock.json in root or subdirs (reasonable depth)
    for current_root, dirs, files in os.walk(root):
        depth = os.path.relpath(current_root, root).count(os.sep)
        if depth > 4:
            dirs[:] = []
            continue
        if 'node_modules' in dirs and current_root != root:
            # avoid deep recursive scans under nested node_modules trees
            dirs.remove('node_modules')
        if 'package-lock.json' in files:
            lock_path = os.path.join(current_root, 'package-lock.json')
            for v in read_package_lock(lock_path):
                findings.append((lock_path, v))

    # Deduplicate
    seen = set()
    uniq = []
    for path, version in findings:
        key = (path, version)
        if key not in seen:
            uniq.append((path, version))
            seen.add(key)
    return uniq


def main() -> int:
    if len(sys.argv) != 2:
        print('UNKNOWN - usage: python3 check_cve_2026_28793.py /path/to/project')
        return 2

    root = sys.argv[1]
    if not os.path.isdir(root):
        print(f'UNKNOWN - path not found: {root}')
        return 2

    findings = find_versions(root)
    if not findings:
        print('UNKNOWN - @tinacms/cli not found in node_modules or package-lock.json')
        return 2

    vulnerable = []
    patched = []
    unknown = []

    for path, version in findings:
        verdict = is_vulnerable(version)
        if verdict is True:
            vulnerable.append((path, version))
        elif verdict is False:
            patched.append((path, version))
        else:
            unknown.append((path, version))

    if vulnerable:
        details = '; '.join([f'{v} @ {p}' for p, v in vulnerable])
        print(f'VULNERABLE - found @tinacms/cli below 2.1.8: {details}')
        return 1

    if patched and not unknown:
        details = '; '.join([f'{v} @ {p}' for p, v in patched])
        print(f'PATCHED - found @tinacms/cli at or above 2.1.8: {details}')
        return 0

    detail_parts = []
    if patched:
        detail_parts.append('patched=' + ', '.join([v for _, v in patched]))
    if unknown:
        detail_parts.append('unparsed=' + ', '.join([v for _, v in unknown]))
    print('UNKNOWN - mixed or unparseable results: ' + ' ; '.join(detail_parts))
    return 2


if __name__ == '__main__':
    sys.exit(main())
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, treat this as a developer-environment cleanup item, not a company-wide emergency outage. Under the noisgate mitigation SLA there is no mitigation SLA — go straight to the 365-day remediation window, but close any obviously exposed Tina dev servers, shared cloud-IDE ports, or 0.0.0.0 binds immediately as sensible hygiene; under the noisgate remediation SLA, finish upgrading all @tinacms/cli instances to 2.1.8 or later within 365 days, with shared dev images, Codespaces/Gitpod templates, and security-sensitive engineering teams first in line.

Sources

  1. NVD CVE-2026-28793
  2. GitHub advisory GHSA-2f24-mg4x-534q
  3. TinaCMS release @tinacms/[email protected]
  4. Companion TinaCMS CORS advisory GHSA-8pw3-9m7f-q734
  5. @tinacms/cli package page
  6. CISA Known Exploited Vulnerabilities Catalog
  7. FIRST EPSS project
  8. OpenCVE record for CVE-2026-28793
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.