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.
4 steps from start to impact.
Reach a live Tina dev server
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.- A developer is actively running
tinacms dev - The attacker can reach the HTTP listener locally or through a forwarded/exposed path
- The installed
@tinacms/cliversion is below2.1.8
- 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
Enumerate files with traversal
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.- HTTP access to the dev server
- Traversal sequences are preserved by the client or proxy
- Some intermediary tooling normalizes paths and breaks traversal
- Enumeration only matters if valuable files or paths exist on the host
../ or unusual access to port 4001.Write or delete arbitrary files
POST /media/upload/... for arbitrary writes and DELETE /media/... for arbitrary deletes, again with curl examples.- The target path is writable or deletable by the Tina process user
- The dev server process has filesystem permissions on the target location
- OS permissions can block writes to sensitive directories
- Not every write leads to code execution; many only cause corruption or data loss
node or the Tina dev process.Turn file access into useful impact
.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.- High-value secrets or executable assets are present on the developer host
- The developer workflow trusts local scripts, builds, or auto-reload behavior
- Impact is usually limited to the single developer box or repo in reach
- A second-stage execution path is environment-specific, not guaranteed
The supporting signals.
| In-the-wild status | No confirmed exploitation surfaced in the reviewed primary sources. It is not listed in the CISA KEV catalog. |
|---|---|
| Proof-of-concept availability | Yes. The vendor's GitHub advisory publishes working curl PoC examples for arbitrary read, write, and delete. |
| EPSS | 0.00034 (~0.034%) from the user-provided intel — extremely low modeled exploitation probability. I did not directly verify the percentile during this review. |
| KEV status | Not KEV-listed in the reviewed CISA Known Exploited Vulnerabilities Catalog. No CISA due date applies. |
| CVSS vector reality check | CVSS: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 versions | Authoritative 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 population | Narrow. 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 coverage | SCA/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 credit | Published 2026-03-12 in GitHub's advisory flow. Reporter credited by the GHSA: alaeddine03. |
noisgate verdict.
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.
Why this verdict
- Downgrade for attacker position:
AV:Lis 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,
.envsecrets, 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.
What to do — in priority order.
- Upgrade
@tinacms/cli— Move all repos, base images, and developer environments to2.1.8or later. Because this is MEDIUM, there is no noisgate mitigation SLA — go straight to the365-day remediation window, but prioritize shared dev images and high-value engineering teams first. - Kill unnecessary exposure paths — Stop publishing Tina dev servers through port-forwarding, cloud IDE sharing links, or
0.0.0.0binds unless there is a documented need. Do this immediately as hygiene even though there is no formal mitigation SLA for a MEDIUM finding. - Watch developer hosts for
/media/abuse — Add temporary detections for suspicious../requests to port4001, unexpectednode-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 the365-day window. - 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.
- 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.
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.
#!/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())
If you remember one thing.
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
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.