A vulnerability in TinaCMS allows attackers to execute code on the server by crafting a malicious markdown file. The issue is caused by the `gray-matter` library, which processes JavaScript in frontmatter by default. Uploading a file with `---js` delimiters leads to RCE. Update to patched versions immediately to disable this functionality.
CVE-2025-68278 is a critical Remote Code Execution (RCE) vulnerability in TinaCMS, a popular headless CMS. The flaw stems from its dependency, the `gray-matter` library, which, in a stunning display of optimism, defaults to executing JavaScript or CoffeeScript found in markdown frontmatter. This allows an attacker with permission to upload a seemingly harmless markdown file to gain complete control of the server, turning a simple content update into a full-scale system compromise. The fix involves explicitly disabling these dangerous 'features,' reminding us that sometimes the most helpful libraries are the ones holding a loaded gun.
TinaCMS positions itself as a slick, Git-backed headless CMS for modern development stacks. It's designed to make content management seamless for both developers and editors. At its core, it wrangles content, often stored in simple markdown files. And like most modern markdown processors, it leans heavily on frontmatter—that little block of metadata, usually YAML, at the top of a file that defines things like the title, author, and publication date.
Frontmatter is supposed to be inert data. A simple, structured key-value store that informs the static site generator how to render the page. Developers have been trained to see it as harmless configuration, as safe as a comment block. It's a trusted, foundational part of the content pipeline. Why would you ever question it?
This is where the story gets interesting. The vulnerability doesn't lie in TinaCMS's own logic, but in a dependency it trusts implicitly: gray-matter. This library is tasked with a simple job: parse the frontmatter. But it came with a hidden, and frankly, ludicrous feature. It didn't just parse data; it could also interpret and execute code. The server process, designed to handle simple text files, was unknowingly equipped with a Node.js execution engine, waiting for the right input.
The root cause of this entire mess is a classic case of a feature becoming a security nightmare. The gray-matter library, in its quest for ultimate flexibility, decided it would be a brilliant idea to support not just YAML, TOML, and JSON for frontmatter, but also JavaScript and CoffeeScript. To 'support' them, it doesn't just parse them as objects; it executes the code within the frontmatter block to generate the final metadata object.
This is CWE-94: Improper Control of Generation of Code, or 'Code Injection,' in its purest form. The library's default behavior is to trust its input so completely that it will run it with the full privileges of the parent process. It's the digital equivalent of a vending machine that accepts handwritten IOUs and dispenses cash. There's no sandbox, no warnings, just straight-up execution.
This design decision is baffling. In what universe does a content editor need to run server-side JavaScript just to define a blog post's title? It violates the principle of least astonishment, turning a data parsing library into an arbitrary code execution engine. For an attacker, this isn't a bug; it's a gift-wrapped feature, a backdoor left wide open by developers who thought they were just parsing text.
Let's pop the hood and look at the code. The scene of the crime is packages/@tinacms/graphql/src/database/util.ts. Before the patch, the code simply called matter(), the main function from gray-matter, without specifying any engine configurations. By default, gray-matter's engine auto-detection would see the ---js delimiter and dutifully switch to its JavaScript execution engine.
The fix, committed in fa7c27abef968e3f3a3e7d564f282bc566087569, is both elegant and simple. The developers didn't try to sanitize the input or create a complex deny-list. They went straight for the kill switch. They explicitly defined a custom set of matterEngines, overriding the dangerous ones.
// The patch in packages/@tinacms/graphql/src/database/util.ts
const matterEngines = {
// ... safe engines like toml
// Disable JavaScript and CoffeeScript execution
js: {
parse: () => {
throw new Error(
'JavaScript execution in frontmatter is not allowed for security reasons'
);
},
// ... same for stringify
},
javascript: { /* ... same override ... */ },
coffee: { /* ... same override ... */ },
coffeescript: { /* ... same override ... */ },
};This change is surgical. Any attempt to parse a markdown file with ---js or ---coffee frontmatter now hits this override. Instead of executing code, the parse function immediately throws a security error, stopping the process cold. It’s a textbook example of secure coding: when a feature is too dangerous to exist, you don't tame it; you remove it entirely. The addition of comprehensive unit tests in util.test.ts to confirm this behavior is the cherry on top, ensuring this ghost won't rise from its grave in a future release.
So, how do we weaponize this? The attack path is terrifyingly simple. Our attacker, let's call her Eve, only needs one thing: the ability to create or edit a markdown file that the TinaCMS server will process. This could be a low-level content editor account, a compromised contractor's credentials, or even a system that ingests markdown from a shared folder.
Eve crafts her malicious blog post. The body of the post is irrelevant; the magic is all in the header. She uses the ---js delimiter to tell gray-matter to switch on its execution engine. The payload is a simple one-liner, disguised within a JSON object definition.
---js
{
"title": "Pawned" + require("fs").readFileSync("/etc/passwd").toString()
}
---
# My Awesome Blog Post
This post is totally safe.When Eve uploads this file, the TinaCMS backend dutifully picks it up. The gray-matter parser sees ---js, enters execution mode, and evaluates the object. To resolve the value for the title key, it must first execute require("fs").readFileSync("/etc/passwd").toString(). The contents of the server's password file are read into memory and appended to the title. While this PoC just logs the file, a real attacker would use child_process.execSync to get a reverse shell, giving them an interactive command prompt on the server.
Here is a simple visualization of the attack flow:
Let's be clear about the impact. This isn't a cross-site scripting flaw or a denial of service. This is full, unauthenticated (from the server's perspective) Remote Code Execution. The attacker gains a foothold on your server with the same privileges as the Node.js process running TinaCMS. From there, the game is over.
The initial compromise is just the beginning. The attacker can read connection strings from environment variables, dump databases, and pivot to attack other internal services that trusted the now-compromised server. They can install crypto miners, deploy ransomware, or simply sit silently and exfiltrate sensitive company data over a period of months. All because a markdown parser had a feature that should never have existed.
The low EPSS score (0.08%) might suggest this is a low-risk vulnerability, but that metric predicts widespread, opportunistic exploitation. It says nothing about the risk of a targeted attack. For an attacker specifically targeting a company known to use TinaCMS, this vulnerability is a goldmine. The attack vector is subtle—who audits the content of markdown files for executable code?
The fix, thankfully, is straightforward: update your dependencies. If you're running TinaCMS, you need to move to tinacms >= 3.1.1, @tinacms/cli >= 2.0.4, and @tinacms/graphql >= 2.0.3. Running npm update or yarn upgrade should be your immediate priority. There is no complex migration path; it's a simple version bump to get the patched code.
The patch itself teaches a valuable lesson in supply chain security. You cannot blindly trust your dependencies, especially not their default configurations. The TinaCMS team did the right thing by explicitly disabling the dangerous engines in gray-matter, adopting a principle of least functionality. Any feature that isn't strictly necessary for your application's purpose is a potential attack surface.
Could this be bypassed? It's unlikely with the current fix, which is an explicit block. An attacker would have to find a vulnerability in one of the allowed engines (like the YAML or TOML parsers), which is a much harder task. However, it's a reminder to always treat user-supplied content, even from authenticated users, as fundamentally untrusted. The line between data and code can be dangerously thin.
For those who absolutely cannot update immediately, a temporary workaround would be to implement a pre-processing check. Your application could scan uploaded markdown files for the strings ---js, ---javascript, ---coffee, or ---coffeescript at the beginning of the file and reject them outright. It's a fragile, temporary band-aid, but it's better than leaving the front door unlocked.
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P| Product | Affected Versions | Fixed Version |
|---|---|---|
tinacms tinacms | < 3.1.1 | 3.1.1 |
@tinacms/cli tinacms | < 2.0.4 | 2.0.4 |
@tinacms/graphql tinacms | < 2.0.3 | 2.0.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-94 |
| Weakness | Improper Control of Generation of Code ('Code Injection') |
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | Low |
| CVSS v4.0 Score | 7.3 (High) |
| EPSS Score | 0.08% (Low Probability) |
| Exploit Status | Proof-of-Concept Available |
| CISA KEV | No |
The software constructs all or part of a code string using externally-controlled input, but it does not neutralize or incorrectly neutralizes the input from a code syntax perspective, which can lead to the injection of arbitrary code.
Get the latest CVE analysis reports delivered to your inbox.