Severity by source
AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L
Lifecycle Timeline
4DescriptionGitHub Advisory
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_charactersWhen 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 limitImpact
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.
AnalysisAI
The replace filter in LiquidJS (Node.js npm package) fails to correctly account for memory usage when memoryLimit is enabled, allowing remote attackers to bypass DoS protections with approximately 2,500x memory amplification by crafting templates where the replace operation produces quadratically larger output than the charged memory cost. Deployments with memoryLimit explicitly configured to protect against untrusted template input can suffer out-of-memory crashes; patch available in v10.25.3.
Technical ContextAI
LiquidJS is a Liquid template engine implementation for JavaScript/Node.js used in multi-tenant SaaS platforms to process user-supplied templates safely. The vulnerability exists in the replace filter (src/filters/string.ts), which uses a memory accounting system (Limiter class in src/util/limiter.ts) to enforce a configurable memoryLimit and prevent denial-of-service attacks. The filter charges only the sum of input string, pattern, and replacement lengths to the memory limiter (str.length + pattern.length + replacement.length bytes), but the actual operation str.split(pattern).join(replacement) produces output sized as (number_of_occurrences × replacement.length) + non_matching_characters. When a pattern occurs repeatedly throughout the input string, the output size grows quadratically while memory charges remain linear, creating a severe mismatch. This is a CWE-400 (Uncontrolled Resource Consumption) vulnerability rooted in incorrect resource accounting logic. The memoryLimit option defaults to Infinity and must be explicitly enabled; only deployments using this feature are vulnerable.
RemediationAI
Vendor-released patch: upgrade to liquidjs v10.25.3 or later. The fix modifies the replace filter to calculate actual output size using const outputSize = str.length + (parts.length - 1) * (replacement.length - pattern.length) before charging the memory limiter, ensuring the memory cost accurately reflects the operation's actual resource consumption. Organizations deploying LiquidJS with memoryLimit enabled should prioritize this update to restore intended DoS protections. Details available in the vendor advisory at https://github.com/harttle/liquidjs/security/advisories/GHSA-mmg9-6m6j-jqqx and the patched commit abc058be0f33d6372cd2216f4945183167abeb25 at https://github.com/harttle/liquidjs/releases/tag/v10.25.3.
FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote
Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t
Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete
Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc
An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner
Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi
Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin
The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic
Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read
Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio
Eval injection vulnerability in index.js in the syntax-error package before 1.1.1 for Node.js 0.10.x, as used in IBM Rat
The HTTP server in Node.js 0.10.x before 0.10.21 and 0.8.x before 0.8.26 allows remote attackers to cause a denial of se
Same weakness CWE-400 – Uncontrolled Resource Consumption
View allSame technique Denial Of Service
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-20554
GHSA-mmg9-6m6j-jqqx