Oct 10, 2026·5 min read·8 visits
Unsanitized Git branch names allow remote code execution in @tinacms/cli during automated CI/CD builds and Vercel preview deployments.
@tinacms/cli prior to version 3.0.0 dynamically constructs client source files using string interpolation without properly sanitizing runtime configuration variables. An attacker with permissions to create a branch or pull request can inject arbitrary JavaScript statements via a crafted Git ref name, leading to execution during automated build processes.
The package @tinacms/cli within the tinacms/tinacms monorepo provides CLI tools and code generation functionality for TinaCMS applications. During the build phase, @tinacms/cli executes automated code generation scripts that emit a TypeScript client configuration file (client.ts). This generated file initializes the TinaCMS client with specific runtime parameters, such as the Content API endpoint URL, authorization tokens, and caching directories.
The dynamic API URL construction mechanism extracts the current Git branch reference from hosting environment variables, including VERCEL_GIT_COMMIT_REF, GITHUB_BRANCH, or the local HEAD ref. In @tinacms/cli versions prior to 3.0.0, these extracted branch names and other configuration properties were directly interpolated into single- or double-quoted string literals within the generated JavaScript source code without escaping.
Because Git reference names permit characters such as single quotes (') and double quotes ("), a remote attacker capable of submitting a pull request or pushing a branch with a specially formatted name can escape the quote boundary. When the build system compiles or executes the generated client.ts module, the injected JavaScript executes with the full privileges of the underlying Node.js runner process.
The fundamental flaw resides in packages/@tinacms/cli/src/next/codegen/index.ts and packages/@tinacms/cli/src/next/codegen/codegen/plugin.ts. The template generation logic uses template string interpolation to emit TypeScript code.
Specifically, the apiURL string is constructed using the active Git ref name. When building the configuration object for createClient, the code generator wraps apiURL, token, cacheDir, and errorPolicy inside literal quote marks:
// Unpatched snippet in index.ts
export const client = createClient({ ${
this.noClientBuildCache === false
? `cacheDir: '${normalizePath(this.configManager.generatedCachePath)}', `
: ''
}url: ${this.localContentBuild ? `process.env.TINA_LOCAL_URL || '${apiURL}'` : `'${apiURL}'`}, token: '${token}', queries, ${
errorPolicy ? `errorPolicy: '${errorPolicy}'` : ''
} });If apiURL contains a single quote, the resulting output file contains premature quote termination. For example, a branch named feature/';require('child_process').execSync('id');// evaluates into the following emitted code:
export const client = createClient({
url: 'https://content.tinajs.io/content/feature/';require('child_process').execSync('id');//',
token: '',
queries
});This constitutes CWE-94 (Improper Control of Generation of Code). The failure to treat variable inputs as literal values allows arbitrary statement injection at module loading time.
To fix the vulnerability, the maintainers modified the code generator in commit d030d414d39e15de79bf36e4c728d57205e71dde. The fix replaces raw quote interpolation with JSON.stringify() calls across all dynamic configuration options.
Below is the patch diff applied to packages/@tinacms/cli/src/next/codegen/index.ts:
@@ -369,12 +369,16 @@ export default databaseClient;
import { queries } from "./types.js";
export const client = createClient({ ${
this.noClientBuildCache === false
- ? `cacheDir: '${normalizePath(
- this.configManager.generatedCachePath
- )}', `
+ ? `cacheDir: ${JSON.stringify(
+ normalizePath(this.configManager.generatedCachePath)
+ )}, `
: ''
- }url: ${this.localContentBuild ? `process.env.TINA_LOCAL_URL || '${apiURL}'` : `'${apiURL}'`}, token: '${token}', queries, ${
- errorPolicy ? `errorPolicy: '${errorPolicy}'` : ''
+ }url: ${
+ this.localContentBuild
+ ? `process.env.TINA_LOCAL_URL || ${JSON.stringify(apiURL)}`
+ : JSON.stringify(apiURL)
+ }, token: ${JSON.stringify(String(token))}, queries, ${
+ errorPolicy ? `errorPolicy: ${JSON.stringify(String(errorPolicy))}` : ''
} });
export default client;By leveraging JSON.stringify(), all dynamic values are converted into valid JSON string literals. Any internal quotes, backslashes, or control characters are automatically escaped (e.g., " becomes \"). This ensures that the generated output is strictly parsed as a string primitive, preventing syntax escape.
> [!NOTE]
> The fix is complete for the identified code generation paths. Replacing string interpolation with explicit string serialization prevents code injection vectors across all dynamic variables in client.ts.
Exploitation relies on an automated deployment pipeline executing @tinacms/cli during branch or pull request preview builds. The attack flow follows these stages:
test';process.env.AWS_SECRET_ACCESS_KEY// or containing network exfiltration payloads.tinacms build or similar CLI commands. The CLI reads the Git ref from environment variables (VERCEL_GIT_COMMIT_REF or GITHUB_BRANCH) and emits the unsanitized string into client.ts.client.ts as part of the frontend compilation step. Node.js evaluates the top-level injected payload, executing code within the host runner.Successful exploitation leads to full arbitrary code execution within the build runner container or Virtual Machine. The vulnerability carries a CVSS 3.1 base score of 8.2 with the vector CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N.
Primary impacts include:
The primary remediation is upgrading @tinacms/cli to version 3.0.0 or higher.
npm install @tinacms/cli@3.0.0package-lock.json, yarn.lock, or pnpm-lock.yaml) reference @tinacms/cli version 3.0.0 or later.If immediate package updates cannot be applied:
VERCEL_GIT_COMMIT_REF, GITHUB_BRANCH) in CI workflow scripts prior to invoking @tinacms/cli commands.CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
@tinacms/cli TinaCMS | < 3.0.0 | 3.0.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-94 (Improper Control of Generation of Code) |
| CVSSv3.1 Score | 8.2 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N |
| Attack Vector | Network |
| Exploit Status | Proof of Concept / Technical Explanation Available |
| CISA KEV Status | Not Listed |
| Affected Component | @tinacms/cli (packages/@tinacms/cli) |
| Fixed Version | 3.0.0 |
The software constructs all or part of a code segment using externally-influenced input, but it does not neutralize or incorrectly neutralizes elements that could alter the syntax or behavior of the intended code segment.
A path traversal vulnerability in Shiny for Python (posit-dev/py-shiny) versions 1.4.0 through 1.6.3 allows unauthenticated remote attackers to read arbitrary files and traverse directories via crafted _state_id_ query parameters.
A Stored Cross-Site Scripting vulnerability in @tinacms/web-components prior to version 0.2.1 allows low-privileged content authors to execute arbitrary JavaScript code in the context of website visitors via unsanitized URL attributes in custom Markdown rendering components.
A critical origin validation flaw in TinaCMS admin preview allows unauthenticated attackers to bypass cross-origin postMessage checks and execute unauthorized GraphQL queries and mutations under an authenticated editor's context.
Nginx UI versions 2.2.0 through 2.5.10 fail to properly configure Gin framework trusted proxies when deployed behind a reverse proxy. This causes all incoming HTTP requests to be attributed to the loopback IP (127.0.0.1), enabling IP allowlist bypass and global authentication lockouts.
Nginx UI versions 2.5.0 through 2.5.10 contain an uncontrolled resource consumption vulnerability in the node authentication handler. Unauthenticated remote attackers can exhaust host disk storage and I/O resources by submitting large HTTP request bodies to node-signature endpoints prior to cryptographic signature validation.
Vikunja versions 2.3.0 through 2.6.0 contain an insufficient session expiration vulnerability (CWE-613) within the WebSocket authentication handler. Although Vikunja enforces server-side session tracking and revocation for REST API routes, the WebSocket handshake handler validates cryptographic JWT signatures without querying the database session state. Consequently, revoked JWT tokens can establish new real-time WebSocket connections, and existing connections persist after session revocation.