Every CVE Search/ Get API key

CVE-2026-33287

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

7.5 HighFix available

LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.1, the replace_first filter in LiquidJS uses JavaScript's String.prototype.replace() which interprets $& as a back reference 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. Version 10.25.1 patches the issue.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
None
Integrity
None
Availability
High

Exploit likelihood

0.48%Up 0.4 points since Mar 26, 2026
90%50%10%1%

What each source reports

CVE List Score used 7.5

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

LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.1, the replace_first filter in LiquidJS uses JavaScript's String.prototype.replace() which interprets $& as a back reference 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. Version 10.25.1 patches the issue.

Record
CVE-2026-33287
CVSS 3.1
7.5 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weakness
CWE-20, CWE-400
Updated
2026-11-02
EUVDEUVD-2026-16064 7.5

LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.1, the replace_first filter in LiquidJS uses JavaScript's String.prototype.replace() which interprets $& as a back reference 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. Version 10.25.1 patches the issue.

Record
EUVD-2026-16064
CVSS 3.1
7.5 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Updated
2026-11-02
GitHub AdvisoriesGHSA-6q5m-63h6-5x4v 7.5

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

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).

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 [email protected]
  • 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 |…
Record
GHSA-6q5m-63h6-5x4v
CVSS 3.1
7.5 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weakness
CWE-20, CWE-400
Updated
2026-11-02
NVD 7.5

LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.1, the replace_first filter in LiquidJS uses JavaScript's String.prototype.replace() which interprets $& as a back reference 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. Version 10.25.1 patches the issue.

Record
CVE-2026-33287
CVSS 3.1
7.5 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weakness
CWE-20, CWE-400
Updated
2026-11-02
OSV 7.5

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

LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.1, the replace_first filter in LiquidJS uses JavaScript's String.prototype.replace() which interprets $& as a back reference 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. Version 10.25.1 patches the issue.

Record
CVE-2026-33287
CVSS 3.1
7.5 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weakness
CWE-20, CWE-400
Updated
2026-11-02
OSVGHSA-6q5m-63h6-5x4v 7.5

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

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).

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 [email protected]
  • 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 |…
Record
GHSA-6q5m-63h6-5x4v
CVSS 3.1
7.5 HighCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weakness
CWE-20, CWE-400
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
liquidjs
npm
before 10.25.1
10.25.1

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-33287 is a high vulnerability (CVSS 7.5) in Harttle Liquidjs, published March 25, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.48%, the 41st percentile. Fixed in liquidjs 10.25.1.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open