CVE-2026-69222 Overview
CVE-2026-69222 is a denial-of-service vulnerability in LiquidJS, a Shopify and GitHub Pages compatible template engine written in pure JavaScript. The join filter in src/filters/array.ts calculates memory complexity from array.length and separator length rather than the actual output size produced by array.join(sep). An attacker can chain the concat filter to cheaply duplicate arrays of references, then invoke join to materialize referenced content while paying only for element count. The same accounting defect exists in array_to_sentence_string in src/filters/string.ts. The flaw is fixed in version 10.27.2 [CWE-400].
Critical Impact
A crafted Liquid template can bypass the configured memoryLimit by a large factor, allocate memory up to V8's string or process limits, and crash the Node.js process.
Affected Products
- LiquidJS versions prior to 10.27.2
- Applications embedding LiquidJS for user-supplied template rendering
- Shopify-compatible template pipelines using vulnerable LiquidJS releases
Discovery Timeline
- 2026-08-19 - CVE-2026-69222 published to NVD
- 2026-08-19 - Last updated in NVD database
- Fix released in LiquidJS v10.27.2 via GitHub Pull Request #925
Technical Details for CVE-2026-69222
Vulnerability Analysis
LiquidJS enforces a memoryLimit to prevent template-driven resource exhaustion. The join filter charged the limit using array.length * (1 + sep.length), which reflects element count but ignores the length of the strings produced when elements are converted and concatenated. The array_to_sentence_string filter contained the same defect. An attacker who controls template content can construct an array of shared string references, use concat to double the array cheaply, and then invoke join to force materialization of the aggregate string. The produced output can far exceed the intended memoryLimit, growing until V8's string length ceiling or the process memory budget is reached, at which point the Node.js runtime crashes.
Root Cause
The accounting function computed complexity from container metadata instead of the size of the materialized result. Because JavaScript arrays can hold references to large strings at negligible cost, element count is not a valid proxy for output length. The memoryLimit.use() call therefore underreported allocation, allowing bounded template execution to produce unbounded output.
Attack Vector
Any system that renders untrusted Liquid templates is exposed over the network. Exploitation requires only that the attacker submit a template containing the vulnerable filter chain. No authentication, user interaction, or local access is needed. The result is a reliable process crash, degrading availability of the hosting service.
// Security patch in src/filters/array.ts (v10.27.2)
// fix: charge join/json/inspect filters by produced output size (#925)
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)
- this.context.memoryLimit.use(complexity)
+ let outputSize = sep.length * Math.max(array.length - 1, 0)
+ for (let i = 0; i < array.length; i++) outputSize += String(array[i]).length
+ this.context.memoryLimit.use(outputSize)
return Array.prototype.join.call(array, sep)
})
Source: GitHub Commit 7ab49f999
// Security patch in src/filters/misc.ts (v10.27.2)
// Charge the memory limit per JSON replacer value
function chargeJsonReplacerValue (memoryLimit: { use(count: number): void }, val: unknown) {
if (typeof val === 'string') {
memoryLimit.use(val.length)
} else if (val === null || typeof val === 'number' || typeof val === 'boolean') {
memoryLimit.use(JSON.stringify(val).length)
} else if (Array.isArray(val)) {
memoryLimit.use(val.length + 1)
} else if (typeof val === 'object') {
memoryLimit.use(2)
}
}
function json (this: FilterImpl, value: any, space = 0) {
const memoryLimit = this.context.memoryLimit
return JSON.stringify(value, (_key, val) => {
chargeJsonReplacerValue(memoryLimit, val)
return val
}, space)
}
Source: GitHub Commit 7ab49f999
Detection Methods for CVE-2026-69222
Indicators of Compromise
- Node.js processes terminating with RangeError: Invalid string length or out-of-memory fatal errors while rendering Liquid templates.
- Repeated crashes of template rendering workers correlated with inbound requests containing concat followed by join or array_to_sentence_string filter chains.
- Sudden spikes in resident set size for LiquidJS worker processes preceding restart events.
Detection Strategies
- Inventory Node.js applications and dependencies to identify liquidjs versions below 10.27.2 using software composition analysis or npm ls liquidjs.
- Inspect application logs for template rendering exceptions referencing memoryLimit, string allocation failures, or worker restarts.
- Add payload inspection rules on template ingest endpoints that flag templates chaining concat with join or array_to_sentence_string on large arrays.
Monitoring Recommendations
- Alert on abnormal Node.js heap growth and V8 fatal error entries in host logs.
- Track HTTP 5xx spikes and process restart counts for services exposing user-supplied template rendering.
- Retain template submission payloads for post-incident analysis when memory-related crashes occur.
How to Mitigate CVE-2026-69222
Immediate Actions Required
- Upgrade LiquidJS to 10.27.2 or later in every affected service and rebuild container images that bundle the package.
- Audit any code paths that accept templates from untrusted users and restrict who can submit Liquid content until the upgrade is deployed.
- Enable process supervisors and rate limiting on template rendering endpoints to contain repeat crash attempts.
Patch Information
The fix is available in LiquidJS v10.27.2. It changes join, array_to_sentence_string, json, and inspect to charge the memoryLimit by produced output size rather than element count. Full details are documented in GitHub Security Advisory GHSA-4r6h-5v86-94p3 and merged via Pull Request #925.
Workarounds
- If immediate upgrade is not possible, disable or filter the join, array_to_sentence_string, json, and inspect filters for untrusted templates.
- Reject templates that combine concat with array-materializing filters, or cap input template size at the ingress layer.
- Run LiquidJS rendering in isolated worker processes with a strict --max-old-space-size so crashes do not affect the main application.
# Upgrade LiquidJS to the patched release
npm install liquidjs@10.27.2 --save
# Verify the resolved version
npm ls liquidjs
# Constrain the Node.js worker to bound the impact of resource-exhaustion attempts
node --max-old-space-size=512 render-worker.js
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

