NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
LOW severity

CVE-2026-34166 — liquidjs

LOWFix: harttle/liquidjs@abc058b

CVE-2026-34166 is a low-severity (CVSS 3.7) Uncontrolled Resource Consumption vulnerability in liquidjs. A fix is available for liquidjs — see the affected versions and patch details below.

LiquidJS has a Memory Limit Bypass via Quadratic Amplification in `replace` Filter

Also known asGHSA-mmg9-6m6j-jqqx
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Nov 9, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-34166.

EPSS Exploitation Probability

via FIRST.org ↗
0.5%probability of exploitation in next 30 days
Lower Risk+0.49%
Lower risk than most CVEs44th percentile — riskier than 44% of all scored CVEsHighest risk
0.00%0.34%0.68%1.01%0.0%0.5%Jun 26Nov 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

Where this sits among everything scored

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

721other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
liquidjsnpm
3.4Mdownloads / week

Description

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

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmliquidjsall versions10.25.3npm install liquidjs@10.25.3

Affected Products

1 product · 1 configurations
Application
liquidjsliquidjs
< 10.25.3
range

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for liquidjs, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update liquidjs to 10.25.3 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-34166 is resolved across your whole dependency graph.

  3. Workarounds

    Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.

Frequently Asked Questions

## 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/filter
O3 Security · Impact-Aware SCA

Is CVE-2026-34166 in your dependencies?

Find it across npm, including transitive dependencies.

CVE-2026-34166: liquidjs DoS — Fixed in 10.25.3