CVE-2026-55575: LiquidJS Remote Code Execution Vulnerability

HIGH
Advisory Identifier: GHSA-g357-x5c3-c72p
CWE-770
Published
Advisory Summary

CVE-2026-55575 is a HIGH remote code execution vulnerability (CWE-770: Allocation of Resources Without Limits or Throttling) affecting LiquidJS libraries in the npm ecosystem. The issue involves `pop` filter bypasses `memorylimit` accounting that its array-filter siblings enforce. Remediation is available by upgrading affected packages to version 10.27.1.

CVE-2026-55575 at a Glance

CVE Identifier CVE-2026-55575
GHSA Identifier GHSA-g357-x5c3-c72p
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-770: Allocation of Resources Without Limits or Throttling
Affected Product LiquidJS
Package Ecosystem npm
Published Date
Last Updated

How to Fix CVE-2026-55575

To remediate this vulnerability, upgrade all affected LiquidJS dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

liquidjs fixed: 10.27.1
npm install liquidjs@10.27.1
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls liquidjs

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-55575, inspect your project dependencies using npm:

npm ls liquidjs

Analyze the terminal output to locate any version of LiquidJS in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 10.27.1), your application is affected.

Note: The npm ls liquidjs command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-770: Allocation of Resources Without Limits or Throttling, where the product allocates a reusable resource or group of resources on behalf of an actor without imposing any intended restrictions on the size or number of resources that can be allocated. In LiquidJS, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

# `pop` filter bypasses `memoryLimit` accounting that its array-filter siblings enforce **CWE**: CWE-770 (Allocation of Resources Without Limits or Throttling) — sibling class of GHSA-8xx9-69p8-7jp3 and GHSA-2546-xv4c-mc8g, applied to `memoryLimit` instead of `renderLimit` ## Summary The `pop` array filter at `src/filters/array.ts:91-95` allocates a full clone of its input array via `[...toArray(v)]` but does **not** call `this.context.memoryLimit.use(...)` the way every other array-clone filter in the same file does (`shift`, `unshift`, `compact`, `concat`, `reverse`, `sample`, `slice`, `map`, `sortBy`, `where`, `group_by`, `uniq`). This silently disables the `memoryLimit` budget for `{{ huge_array | pop }}`, letting a template render allocate an O(N) clone of an attacker-influenced array regardless of how strictly `memoryLimit` is set. ## Affected - liquidjs ≥ all versions that ship the current `pop` filter implementation (verified `10.27.0`, HEAD `a8fd734b5`) - Deployments where any template uses `{{ arr | pop }}` on an array whose length is influenced by untrusted input (typical multi-tenant context arrays: orders, log lines, catalog entries, user lists, etc.) ## Vulnerability details ### Code `src/filters/array.ts:91-95`: ```ts export function pop<T> (v: T[]): T[] { const clone = [...toArray(v)] // O(N) allocation — not charged to memoryLimit clone.pop() return clone } ``` Note: the function signature does not even declare `this: FilterImpl`, so it has no typed access to `this.context.memoryLimit` at the type level — a visual tell that the author skipped the limit-accounting boilerplate the surrounding filters use. Compare with `shift` (`src/filters/array.ts:97-103`), which is functionally identical except for the array-end operated on: ```ts export function shift<T> (this: FilterImpl, v: T[]): T[] { const array = toArray(v) this.context.memoryLimit.use(array.length) // ← guard present const clone = [...array] clone.shift() return clone } ``` And `unshift`, `compact`, `concat`, `reverse`, `sample`, `slice`, `map`, `sortBy`, `where`, `group_by`, `uniq` — all of which also charge `memoryLimit.use(array.length)` (or `lhs.length + rhs.length` etc.) before allocating their working buffer. The asymmetry confirms `pop` is an accidental omission, not by design. ### Why the bypass matters `memoryLimit` is the documented control for bounding the memory a single `render()` call may allocate (`docs/source/tutorials/dos.md`). Every array-output filter in `src/filters/array.ts` other than `pop` deducts its working set from the limit, so a render that does `{{ huge | shift }}` with `memoryLimit: 100` and `huge.length === 5_000_000` correctly throws `memory alloc limit exceeded`. The identical `{{ huge | pop }}` does **not** throw — the allocation proceeds, and the only ceiling is the Node process's heap. ## Proof of concept ```js const { Liquid } = require('liquidjs'); const l = new Liquid({ memoryLimit: 100 }); // 100-unit budget const huge = Array(5_000_000).fill('x'); // 5M-element context array (async () => { try { await l.parseAndRender('{{ a | shift | size }}', { a: huge }); } catch (e) { console.log('shift: ' + e.message); } // expected: memory alloc limit exceeded try { await l.parseAndRender('{{ a | unshift: 0 | size }}', { a: huge }); } catch (e) { console.log('unshift: ' + e.message); } // expected: memory alloc limit exceeded const out = await l.parseAndRender('{{ a | pop | size }}', { a: huge }); console.log('pop: OK, size=' + out); // size=4999999 — allocation succeeded })(); ``` Observed (against `dist/liquid.node.js` at `a8fd734b5`): ``` shift: memory alloc limit exceeded, line:1, col:1 unshift: memory alloc limit exceeded, line:1, col:1 pop: OK, size=4999999 ``` ## Impact - **`memoryLimit` does not bound `pop` allocations.** Any template that can reach `{{ <untrusted-sized array> | pop }}` allocates an O(N) clone outside the budget. - **Realistic attack surface**: when a server passes an attacker-influenced large array to the template context (search results, paginated lists, batch-export pages) and the template uses `| pop` anywhere on it, a single render can allocate hundreds of MB of array slots that the operator believed `memoryLimit` had ruled out. - **Concurrent amplification**: N parallel requests each allocate their own unguarded clone — the practical ceiling is the Node process heap, after which the host runs `oom-kill`. This is the same outcome the renderLimit-empty-body advisories (GHSA-8xx9-69p8-7jp3 / GHSA-2546-xv4c-mc8g) prevented for CPU; this report prevents it for memory. Severity is configuration-dependent (requires `memoryLimit` to be set, plus a template that uses `pop`, plus attacker-influenced array length). For deployments that rely on `memoryLimit` as a DoS guard, this is a real bypass of that guard. ## Workaround for users Until a fix lands, deployments relying on `memoryLimit` should either: - Avoid `| pop` in templates whose inputs include untrusted-length arrays. Use `| slice: 0, arr.size | minus: 1` or equivalent guarded alternatives. - Register a wrapping `pop` filter that does the accounting: ```js liquid.registerFilter('pop', function (v) { const arr = Array.from(v ?? []); this.context.memoryLimit.use(arr.length); arr.pop(); return arr; }); ``` ## Suggested fix One-line addition mirroring `shift`: ```ts export function pop<T> (this: FilterImpl, v: T[]): T[] { const array = toArray(v) this.context.memoryLimit.use(array.length) // ← add this line, and add `this: FilterImpl` const clone = [...array] clone.pop() return clone } ``` No API or behavior change for callers within budget; rejects out-of-budget calls with the standard `memory alloc limit exceeded` exception the sibling filters already throw.

CVE-2026-55575 Affected LiquidJS Versions

1 component(s)
Fixed Versions:
10.27.1
Product / Package Ecosystem Affected Versions Fixed Version
liquidjs
npm
< 10.27.1
10.27.1

References

Frequently Asked Questions

What is CVE-2026-55575?
CVE-2026-55575 is a security vulnerability in LiquidJS involving `pop` filter bypasses `memoryLimit` accounting that its array-filter siblings enforce under CWE-770: Allocation of Resources Without Limits or Throttling.
Which LiquidJS versions are affected?
Affected versions include releases prior to 10.27.1 across affected packages: liquidjs.
What version fixes CVE-2026-55575?
Upgrading to 10.27.1 (or the latest patched release for your respective branch) resolves the vulnerability.
Is liquidjs affected?
Yes, liquidjs is affected in releases prior to 10.27.1. Upgrade your dependency to 10.27.1 or later.
What CWE is associated with CVE-2026-55575?
CVE-2026-55575 is classified under CWE-770: Allocation of Resources Without Limits or Throttling (The product allocates a reusable resource or group of resources on behalf of an actor without imposing any intended restrictions on the size or number of resources that can be allocated.).
How can I check npm dependencies for CVE-2026-55575?
Run the dependency tree command: 'npm ls liquidjs'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-g357-x5c3-c72p",
  "modified" : "2026-07-24T14:05:20Z",
  "published" : "2026-07-24T14:05:20Z",
  "aliases" : [ "CVE-2026-55575" ],
  "summary" : "LiquidJS: `pop` filter bypasses `memoryLimit` accounting that its array-filter siblings enforce",
  "details" : "# `pop` filter bypasses `memoryLimit` accounting that its array-filter siblings enforce\n\n**CWE**: CWE-770 (Allocation of Resources Without Limits or Throttling) — sibling class of GHSA-8xx9-69p8-7jp3 and GHSA-2546-xv4c-mc8g, applied to `memoryLimit` instead of `renderLimit`\n\n## Summary\n\nThe `pop` array filter at `src/filters/array.ts:91-95` allocates a full clone of its input array via `[...toArray(v)]` but does **not** call `this.context.memoryLimit.use(...)` the way every other array-clone filter in the same file does (`shift`, `unshift`, `compact`, `concat`, `reverse`, `sample`, `slice`, `map`, `sortBy`, `where`, `group_by`, `uniq`). This silently disables the `memoryLimit` budget for `{{ huge_array | pop }}`, letting a template render allocate an O(N) clone of an attacker-influenced array regardless of how strictly `memoryLimit` is set.\n\n## Affected\n\n- liquidjs ≥ all versions that ship the current `pop` filter implementation (verified `10.27.0`, HEAD `a8fd734b5`)\n- Deployments where any template uses `{{ arr | pop }}` on an array whose length is influenced by untrusted input (typical multi-tenant context arrays: orders, log lines, catalog entries, user lists, etc.)\n\n## Vulnerability details\n\n### Code\n\n`src/filters/array.ts:91-95`:\n\n```ts\nexport function pop<T> (v: T[]): T[] {\n  const clone = [...toArray(v)]   // O(N) allocation — not charged to memoryLimit\n  clone.pop()\n  return clone\n}\n```\n\nNote: the function signature does not even declare `this: FilterImpl`, so it has no typed access to `this.context.memoryLimit` at the type level — a visual tell that the author skipped the limit-accounting boilerplate the surrounding filters use.\n\nCompare with `shift` (`src/filters/array.ts:97-103`), which is functionally identical except for the array-end operated on:\n\n```ts\nexport function shift<T> (this: FilterImpl, v: T[]): T[] {\n  const array = toArray(v)\n  this.context.memoryLimit.use(array.length)   // ← guard present\n  const clone = [...array]\n  clone.shift()\n  return clone\n}\n```\n\nAnd `unshift`, `compact`, `concat`, `reverse`, `sample`, `slice`, `map`, `sortBy`, `where`, `group_by`, `uniq` — all of which also charge `memoryLimit.use(array.length)` (or `lhs.length + rhs.length` etc.) before allocating their working buffer.\n\nThe asymmetry confirms `pop` is an accidental omission, not by design.\n\n### Why the bypass matters\n\n`memoryLimit` is the documented control for bounding the memory a single `render()` call may allocate (`docs/source/tutorials/dos.md`). Every array-output filter in `src/filters/array.ts` other than `pop` deducts its working set from the limit, so a render that does `{{ huge | shift }}` with `memoryLimit: 100` and `huge.length === 5_000_000` correctly throws `memory alloc limit exceeded`. The identical `{{ huge | pop }}` does **not** throw — the allocation proceeds, and the only ceiling is the Node process's heap.\n\n## Proof of concept\n\n```js\nconst { Liquid } = require('liquidjs');\n\nconst l    = new Liquid({ memoryLimit: 100 });        // 100-unit budget\nconst huge = Array(5_000_000).fill('x');              // 5M-element context array\n\n(async () => {\n  try { await l.parseAndRender('{{ a | shift | size }}', { a: huge }); }\n  catch (e) { console.log('shift:   ' + e.message); }      // expected: memory alloc limit exceeded\n\n  try { await l.parseAndRender('{{ a | unshift: 0 | size }}', { a: huge }); }\n  catch (e) { console.log('unshift: ' + e.message); }      // expected: memory alloc limit exceeded\n\n  const out = await l.parseAndRender('{{ a | pop | size }}', { a: huge });\n  console.log('pop:     OK, size=' + out);                  // size=4999999 — allocation succeeded\n})();\n```\n\nObserved (against `dist/liquid.node.js` at `a8fd734b5`):\n\n```\nshift:   memory alloc limit exceeded, line:1, col:1\nunshift: memory alloc limit exceeded, line:1, col:1\npop:     OK, size=4999999\n```\n\n## Impact\n\n- **`memoryLimit` does not bound `pop` allocations.** Any template that can reach `{{ <untrusted-sized array> | pop }}` allocates an O(N) clone outside the budget.\n- **Realistic attack surface**: when a server passes an attacker-influenced large array to the template context (search results, paginated lists, batch-export pages) and the template uses `| pop` anywhere on it, a single render can allocate hundreds of MB of array slots that the operator believed `memoryLimit` had ruled out.\n- **Concurrent amplification**: N parallel requests each allocate their own unguarded clone — the practical ceiling is the Node process heap, after which the host runs `oom-kill`. This is the same outcome the renderLimit-empty-body advisories (GHSA-8xx9-69p8-7jp3 / GHSA-2546-xv4c-mc8g) prevented for CPU; this report prevents it for memory.\n\nSeverity is configuration-dependent (requires `memoryLimit` to be set, plus a template that uses `pop`, plus attacker-influenced array length). For deployments that rely on `memoryLimit` as a DoS guard, this is a real bypass of that guard.\n\n## Workaround for users\n\nUntil a fix lands, deployments relying on `memoryLimit` should either:\n\n- Avoid `| pop` in templates whose inputs include untrusted-length arrays. Use `| slice: 0, arr.size | minus: 1` or equivalent guarded alternatives.\n- Register a wrapping `pop` filter that does the accounting:\n\n  ```js\n  liquid.registerFilter('pop', function (v) {\n    const arr = Array.from(v ?? []);\n    this.context.memoryLimit.use(arr.length);\n    arr.pop();\n    return arr;\n  });\n  ```\n\n## Suggested fix\n\nOne-line addition mirroring `shift`:\n\n```ts\nexport function pop<T> (this: FilterImpl, v: T[]): T[] {\n  const array = toArray(v)\n  this.context.memoryLimit.use(array.length)   // ← add this line, and add `this: FilterImpl`\n  const clone = [...array]\n  clone.pop()\n  return clone\n}\n```\n\nNo API or behavior change for callers within budget; rejects out-of-budget calls with the standard `memory alloc limit exceeded` exception the sibling filters already throw.",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "liquidjs"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "10.27.1"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 10.27.0"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/harttle/liquidjs/security/advisories/GHSA-g357-x5c3-c72p"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-55575"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/harttle/liquidjs/pull/907"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/harttle/liquidjs/commit/8a0c74a7fcb1671aa1dcb71ec82ba0602dc90d04"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/harttle/liquidjs"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/harttle/liquidjs/releases/tag/v10.27.1"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-770" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-24T14:05:20Z",
    "nvd_published_at" : "2026-07-08T20:16:53Z"
  }
}