CVE-2026-34166
LiquidJS has a Memory Limit Bypass via Quadratic Amplification in `replace` Filter
CVE-2026-34166 is a low vulnerability (CVSS 3.7) in Harttle Liquidjs, published April 8, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.50%, the 42nd percentile. Fixed in liquidjs 10.25.3.
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.3, the replace filter in LiquidJS incorrectly accounts for memory usage when the memoryLimit option is enabled. It charges str.length + pattern.length + replacement.length bytes to the memory limiter, but the actual output from str.split(pattern).join(replacement) can be quadratically larger when the pattern occurs many times in the input string. This allows an attacker who controls template content to bypass the memoryLimit DoS protection with approximately 2,500x amplification, potentially causing out-of-memory conditions. This vulnerability is fixed in 10.25.3.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- Low
Exploit likelihood
What each source reports
CVE List Score used
3.7
LiquidJS has a Memory Limit Bypass via Quadratic Amplification in `replace` Filter
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.3, the replace filter in LiquidJS incorrectly accounts for memory usage when the memoryLimit option is enabled. It charges str.length + pattern.length + replacement.length bytes to the memory limiter, but the actual output from str.split(pattern).join(replacement) can be quadratically larger when the pattern occurs many times in the input string. This allows an attacker who controls template content to bypass the memoryLimit DoS protection with approximately 2,500x amplification, potentially causing out-of-memory conditions. This vulnerability is fixed in 10.25.3.
- Record
CVE-2026-34166- CVSS 3.1
- 3.7 Low
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-400
- Updated
- 2026-11-02
EUVDEUVD-2026-20554
3.7
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.3, the replace filter in LiquidJS incorrectly accounts for memory usage when the memoryLimit option is enabled. It charges str.length + pattern.length + replacement.length bytes to the memory limiter, but the actual output from str.split(pattern).join(replacement) can be quadratically larger when the pattern occurs many times in the input string. This allows an attacker who controls template content to bypass the memoryLimit DoS protection with approximately 2,500x amplification, potentially causing out-of-memory conditions. This vulnerability is fixed in 10.25.3.
- Record
EUVD-2026-20554- CVSS 3.1
- 3.7 Low
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-mmg9-6m6j-jqqx
3.7
LiquidJS Has Memory Limit Bypass via Quadratic Amplification in `replace` Filter
Summary
The replace filter in LiquidJS incorrectly accounts for memory usage when the memoryLimit option is enabled. It charges str.length + pattern.length + replacement.length bytes to the memory limiter, but the actual output from str.split(pattern).join(replacement) can be quadratically larger when the pattern occurs many times in the input string. This allows an attacker who controls template content to bypass the memoryLimit DoS protection with approximately 2,500x amplification, potentially causing out-of-memory conditions.
Details
The vulnerable code is in src/filters/string.ts:137-142:
export function replace (this: FilterImpl, v: string, pattern: string, replacement: string) {
const str = stringify(v)
pattern = stringify(pattern)
replacement = stringify(replacement)
this.context.memoryLimit.use(str.length + pattern.length + replacement.length) // BUG: accounts for inputs, not output
return str.split(pattern).join(replacement) // actual output can be quadratically larger
}
The memoryLimit.use() call charges only the sum of the three input lengths. However, the str.split(pattern).join(replacement) operation produces output of size:
(number_of_occurrences * replacement.length) + non_matching_characters
When every character in str matches pattern (e.g., str = 5,000 as, pattern = a), there are 5,000 occurrences. With a 5,000-character replacement string, the output is 5000 * 5000 = 25,000,000 characters, while only 5000 + 1 + 5000 = 10,001 bytes are charged to the limiter.
The Limiter class at src/util/limiter.ts:3-22 is a simple accumulator — it only checks at the time use() is called and has no post-hoc validation of actual memory allocated.
The memoryLimit option defaults to Infinity (src/liquid-options.ts:198), so this only affects deployments that explicitly enable memory limiting to protect against untrusted template input.
PoC
const { Liquid } = require('liquidjs');
// User explicitly enables memoryLimit for DoS protection (10MB)
const engine = new Liquid({ memoryLimit: 1e7 });
const inputLen = 5000;
const aStr = 'a'.repeat(inputLen);
const bStr = 'b'.repeat(inputLen);
// Template that should be blocked by 10MB memory limit
const tpl = engine.parse(
`{%- assign s = "${aStr}" -%}` +
`{%- assign r = "${bStr}" -%}` +
`{{ s | replace: "a", r }}`
);
// This should throw "memory alloc limit exceeded" but succeeds
const result = engine.renderSync(tpl);
console.log('Memory limit: 10,000,000 bytes');
console.log('Memory charged:', 10001, 'bytes');
console.log('Actual output:', result.length, 'bytes'); // 25,000,000 bytes
console.log('Amplification:', Math.round(result.length / 10001) + 'x');
// Output: Amplification: 2500x — completely bypasses the 10MB limit
Impact
Users who deploy LiquidJS with memoryLimit enabled to process untrusted templates (e.g., multi-tenant SaaS platforms allowing custom templates) are not protected against memory exhaustion via the replace filter. An attacker who can author templates can allocate ~2,500x more memory than the configured limit allows, potentially causing:
- Node.js process out-of-memory crashes
- Denial of service for co-tenant users on the same process
- Resource exhaustion on the hosting infrastructure
The impact is limited to availability (no confidentiality or integrity impact), and requires both non-default configuration (memoryLimit enabled) and template authoring access.
Recommended Fix
Account for the actual output size in the memory limiter by calculating the number of occurrences:
export function replace (this: FilterImpl, v: string, pattern: string, replacement: string) {
const str = stringify(v)
pattern = stringify(pattern)
replacement = stringify(replacement)
const parts = str.split(pattern)
const outputSize = str.length + (parts.length - 1) * (replacement.length - pattern.length)
…
- Record
GHSA-mmg9-6m6j-jqqx- CVSS 3.1
- 3.7 Low
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-400
- Updated
- 2026-11-02
NVD
3.7
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.3, the replace filter in LiquidJS incorrectly accounts for memory usage when the memoryLimit option is enabled. It charges str.length + pattern.length + replacement.length bytes to the memory limiter, but the actual output from str.split(pattern).join(replacement) can be quadratically larger when the pattern occurs many times in the input string. This allows an attacker who controls template content to bypass the memoryLimit DoS protection with approximately 2,500x amplification, potentially causing out-of-memory conditions. This vulnerability is fixed in 10.25.3.
- Record
CVE-2026-34166- CVSS 3.1
- 3.7 Low
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L - CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-400
- Updated
- 2026-11-02
OSV
3.7
LiquidJS has a Memory Limit Bypass via Quadratic Amplification in `replace` Filter
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.3, the replace filter in LiquidJS incorrectly accounts for memory usage when the memoryLimit option is enabled. It charges str.length + pattern.length + replacement.length bytes to the memory limiter, but the actual output from str.split(pattern).join(replacement) can be quadratically larger when the pattern occurs many times in the input string. This allows an attacker who controls template content to bypass the memoryLimit DoS protection with approximately 2,500x amplification, potentially causing out-of-memory conditions. This vulnerability is fixed in 10.25.3.
- Record
CVE-2026-34166- CVSS 3.1
- 3.7 Low
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-400
- Updated
- 2026-11-02
OSVGHSA-mmg9-6m6j-jqqx
3.7
LiquidJS Has Memory Limit Bypass via Quadratic Amplification in `replace` Filter
Summary
The replace filter in LiquidJS incorrectly accounts for memory usage when the memoryLimit option is enabled. It charges str.length + pattern.length + replacement.length bytes to the memory limiter, but the actual output from str.split(pattern).join(replacement) can be quadratically larger when the pattern occurs many times in the input string. This allows an attacker who controls template content to bypass the memoryLimit DoS protection with approximately 2,500x amplification, potentially causing out-of-memory conditions.
Details
The vulnerable code is in src/filters/string.ts:137-142:
export function replace (this: FilterImpl, v: string, pattern: string, replacement: string) {
const str = stringify(v)
pattern = stringify(pattern)
replacement = stringify(replacement)
this.context.memoryLimit.use(str.length + pattern.length + replacement.length) // BUG: accounts for inputs, not output
return str.split(pattern).join(replacement) // actual output can be quadratically larger
}
The memoryLimit.use() call charges only the sum of the three input lengths. However, the str.split(pattern).join(replacement) operation produces output of size:
(number_of_occurrences * replacement.length) + non_matching_characters
When every character in str matches pattern (e.g., str = 5,000 as, pattern = a), there are 5,000 occurrences. With a 5,000-character replacement string, the output is 5000 * 5000 = 25,000,000 characters, while only 5000 + 1 + 5000 = 10,001 bytes are charged to the limiter.
The Limiter class at src/util/limiter.ts:3-22 is a simple accumulator — it only checks at the time use() is called and has no post-hoc validation of actual memory allocated.
The memoryLimit option defaults to Infinity (src/liquid-options.ts:198), so this only affects deployments that explicitly enable memory limiting to protect against untrusted template input.
PoC
const { Liquid } = require('liquidjs');
// User explicitly enables memoryLimit for DoS protection (10MB)
const engine = new Liquid({ memoryLimit: 1e7 });
const inputLen = 5000;
const aStr = 'a'.repeat(inputLen);
const bStr = 'b'.repeat(inputLen);
// Template that should be blocked by 10MB memory limit
const tpl = engine.parse(
`{%- assign s = "${aStr}" -%}` +
`{%- assign r = "${bStr}" -%}` +
`{{ s | replace: "a", r }}`
);
// This should throw "memory alloc limit exceeded" but succeeds
const result = engine.renderSync(tpl);
console.log('Memory limit: 10,000,000 bytes');
console.log('Memory charged:', 10001, 'bytes');
console.log('Actual output:', result.length, 'bytes'); // 25,000,000 bytes
console.log('Amplification:', Math.round(result.length / 10001) + 'x');
// Output: Amplification: 2500x — completely bypasses the 10MB limit
Impact
Users who deploy LiquidJS with memoryLimit enabled to process untrusted templates (e.g., multi-tenant SaaS platforms allowing custom templates) are not protected against memory exhaustion via the replace filter. An attacker who can author templates can allocate ~2,500x more memory than the configured limit allows, potentially causing:
- Node.js process out-of-memory crashes
- Denial of service for co-tenant users on the same process
- Resource exhaustion on the hosting infrastructure
The impact is limited to availability (no confidentiality or integrity impact), and requires both non-default configuration (memoryLimit enabled) and template authoring access.
Recommended Fix
Account for the actual output size in the memory limiter by calculating the number of occurrences:
export function replace (this: FilterImpl, v: string, pattern: string, replacement: string) {
const str = stringify(v)
pattern = stringify(pattern)
replacement = stringify(replacement)
const parts = str.split(pattern)
const outputSize = str.length + (parts.length - 1) * (replacement.length - pattern.length)
…
- Record
GHSA-mmg9-6m6j-jqqx- CVSS 3.1
- 3.7 Low
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-400
- Updated
- 2026-11-02
Affected products
Affected packages
History
| When | Changed | Sources |
|---|---|---|
| 2026-11-02 | created | CVE List, EUVD, GitHub Advisories, NVD, OSV |
References
github.com/harttle/liquidjs/security/advisories/GHSA-mmg9-6m6j-jqqx
github.com/harttle/liquidjs/commit/abc058be0f33d6372cd2216f4945183167abeb25
github.com/harttle/liquidjs/releases/tag/v10.25.3
nvd.nist.gov/vuln/detail/CVE-2026-34166
github.com/advisories/GHSA-mmg9-6m6j-jqqx
github.com/CVEProject/cvelistV5/tree/main/cves/2026/34xxx/CVE-2026-34166.json
github.com/harttle/liquidjs
Left out here: the rest of 2 source descriptions. The JSON record has them.