Oct 10, 2026·5 min read·5 visits
TinaCMS admin preview dynamically loaded external origins from hash routing parameters and trusted postMessage traffic from those origins, allowing remote attackers to run arbitrary GraphQL operations on behalf of logged-in editors.
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.
TinaCMS is an open-source headless Content Management System (CMS) that integrates real-time editing and content management features directly into React applications. A security flaw identified as CVE-2026-108261 exists in the admin preview routing interface of TinaCMS packages tinacms and @tinacms/app.
The vulnerability stems from insufficient validation of client-side routing parameters when constructing the embedded preview frame. When rendering the admin interface, the application extracts path information from the URL hash fragment and injects it into an <iframe> element intended to preview published or drafted content.
By crafting the hash fragment in a link, an unauthenticated remote attacker can force the host application to load an external, attacker-controlled origin into the preview frame while simultaneously trusting cross-origin postMessage requests from that frame.
The root cause of CVE-2026-108261 lies in two distinct implementation flaws: unsafe routing parameter prepending and dynamic self-referential origin determination for postMessage security.
In packages/tinacms/src/admin/index.tsx, the admin preview route uses React Router's splat parameter (params['*']) to set the source URL for the preview frame. The application originally initialized this source URL by prepending a single forward slash to the splat parameter: /${params['*']}. When an attacker provides leading slashes or backslashes in the hash fragment (such as #/~//attacker.com), string concatenation converts the relative path into a protocol-relative absolute URL (//attacker.com).
Simultaneously, packages/@tinacms/app/src/lib/preview-origin.ts implemented getExpectedPreviewOrigin, which extracted the expected postMessage origin directly from the iframe URL itself. When the iframe source was set to //attacker.com, new URL('//attacker.com', baseOrigin).origin resolved to https://attacker.com. As a result, the admin GraphQL reducer set its expected communication origin to https://attacker.com, accepting arbitrary administrative GraphQL read and write requests initiated by the external page.
To remediate the vulnerability, commit b57dbf4b56201aef15cd92caa49fd12ab96bbecf refactored origin determination and added strict path resolution checks.
In @tinacms/app, getExpectedPreviewOrigin was removed and replaced with getPreviewOrigin(), enforcing that the expected frame origin strictly matches window.location.origin without inspecting the target iframe URL:
// packages/@tinacms/app/src/lib/preview-origin.ts
// Patched implementation enforcing static origin trust
export const getPreviewOrigin = (): string =>
typeof window !== 'undefined' ? window.location.origin : '';In packages/tinacms/src/admin/preview-url.ts, the developer implemented resolvePreviewPath to perform double-pass resolution validation using native URL objects. This prevents protocol-relative bypasses, backslashes, leading tabs, and path traversal tricks:
// packages/tinacms/src/admin/preview-url.ts
export function resolvePreviewPath(
splat: string | undefined,
baseOrigin: string = typeof window !== 'undefined' ? window.location.origin : ''
): { path: string; offOrigin: boolean } {
if (!baseOrigin || !splat) {
return { path: '/', offOrigin: false };
}
try {
const resolved = new URL(`/${splat}`, baseOrigin);
if (resolved.origin !== baseOrigin) {
return { path: '/', offOrigin: true };
}
const path = `${resolved.pathname}${resolved.search}${resolved.hash}`;
// Second pass catches path normalization tricks like ..//
if (new URL(path, baseOrigin).origin !== baseOrigin) {
return { path: '/', offOrigin: true };
}
return { path, offOrigin: false };
} catch {
return { path: '/', offOrigin: true };
}
}Exploitation requires an unauthenticated attacker to distribute a malicious link to an authenticated CMS editor. The link targets the host application's admin route with a protocol-relative URL in the hash parameter.
When the authenticated victim navigates to https://admin.example.com/#/~//attacker.com, the React Router splat parameter captures //attacker.com. The unpatched code sets the iframe src to //attacker.com, forcing the victim's browser to load the attacker's web page inside the TinaCMS admin interface.
The attacker's hosted page sends cross-window messages via window.parent.postMessage(). Because getExpectedPreviewOrigin dynamically adopted https://attacker.com as its expected origin, the parent window's GraphQL message handler accepts the incoming payload and executes arbitrary queries or mutations using the editor's active session context.
CVE-2026-108261 carries a CVSS v3.1 score of 9.3 (Critical) with vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N. The primary security scope change occurs because an external cross-origin iframe is granted administrative access to the primary host application's GraphQL communication channel.
Successful exploitation allows the attacker to read, modify, or delete any content managed by the CMS instance. The attacker can execute arbitrary GraphQL mutations to alter site structure, inject malicious code into published posts, or extract confidential operational data.
While the attack requires user interaction (a logged-in editor clicking a link), it requires zero initial privileges on the CMS. No direct system infrastructure denial of service is accomplished through this specific vulnerability.
To eliminate the vulnerability, organizations must update affected npm packages to fixed releases. The patch is available in tinacms version 3.14.0 and @tinacms/app version 2.5.14.
Update dependencies using the appropriate package manager command:
npm install tinacms@3.14.0 @tinacms/app@2.5.14In addition to updating dependencies, security teams should monitor Web Application Firewall (WAF) or web server access logs for anomalous request patterns in hash fragments, specifically looking for double forward slashes (/#/~//) or backslashes (/#/~/\) within the admin URI paths.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
tinacms TinaCMS | < 3.14.0 | 3.14.0 |
@tinacms/app TinaCMS | < 2.5.14 | 2.5.14 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-108261 |
| CWE ID | CWE-346 (Origin Validation Error) |
| CVSS v3.1 | 9.3 (Critical) |
| Attack Vector | Network (Requires User Interaction) |
| Impact | Full Administrative GraphQL Read/Write Access |
| Exploit Status | Proof of Concept / Public Advisory |
| CISA KEV Status | Not Listed |
The software does not properly validate that the source of data or communication is valid.
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.
@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.
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.