CVE-2026-45357: Liquidjs LiquidJS Has A Memory And Vulnerability

HIGH
Advisory Identifier: GHSA-hh27-hf48-9f5q
CVSS 7.5
CWE-400
Published
Advisory Summary

CVE-2026-45357 is a HIGH (CVSS 7.5) liquidjs has a memory and vulnerability (CWE-400: Uncontrolled Resource Consumption) affecting Liquidjs libraries in the npm ecosystem. The issue involves liquidjs has a memory and render limit bypass via unbounded width padding in `date` filter (strftime). Remediation is available in the latest vendor maintenance releases.

CVE-2026-45357 at a Glance

CVE Identifier CVE-2026-45357
GHSA Identifier GHSA-hh27-hf48-9f5q
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-400: Uncontrolled Resource Consumption
Affected Product Liquidjs
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2026-45357

To remediate this vulnerability, upgrade all affected Liquidjs dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

liquidjs fixed: Latest secure release
npm install liquidjs@Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls liquidjs

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-45357, inspect your project dependencies using npm:

npm ls liquidjs

Analyze the terminal output to locate any version of Liquidjs in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: The npm ls liquidjs command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & RFC 5280 Specification

Under RFC 5280 specifications for X.509 certificates, the Name Constraints extension defines a namespace within which all subsequent subject names in a certification path must be located. Constraints apply to various name forms including directory names, IP addresses, rfc822Name (email addresses), and uniformResourceIdentifier (URI) entries. Specifically, during certificate path validation, affected versions of Liquidjs failed to properly normalize or enforce boundary checks when domain names contained trailing dot (.) characters. Because domain names with a trailing dot signify fully qualified domain names (FQDNs), this omission allowed non-conforming certificates to be improperly validated as compliant with the defined name constraints.

Source Advisory Statement

## 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 liquidjs@10.25.7`. 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.

CVE-2026-45357 Affected Liquidjs Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
liquidjs
npm
≤ 10.25.7

References

Frequently Asked Questions

What is CVE-2026-45357?
CVE-2026-45357 is a security vulnerability in Liquidjs involving LiquidJS has a memory and render limit bypass via unbounded width padding in `date` filter (strftime) under CWE-400: Uncontrolled Resource Consumption.
Which Liquidjs versions are affected?
Affected versions include releases prior to current release patches across affected packages: liquidjs.
What version fixes CVE-2026-45357?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is liquidjs affected?
Yes, liquidjs is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of CVE-2026-45357?
CVE-2026-45357 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-45357?
CVE-2026-45357 is classified under CWE-400: Uncontrolled Resource Consumption (The product does not properly control the allocation and maintenance of a limited resource.).
How can I check npm dependencies for CVE-2026-45357?
Run the dependency tree command: 'npm ls liquidjs'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-hh27-hf48-9f5q",
  "modified" : "2026-07-09T21:06:30Z",
  "published" : "2026-05-27T17:33:52Z",
  "aliases" : [ "CVE-2026-45357" ],
  "summary" : "LiquidJS has a memory and render limit bypass via unbounded width padding in `date` filter (strftime)",
  "details" : "## Summary\n\nThe `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.\n\n## Details\n\n`date.ts:5-13` only charges `memoryLimit` for the lengths of the input value, format string, and timezone:\n\n```ts\nexport function date (this: FilterImpl, v: string | Date, format?: string, timezoneOffset?: number | string) {\n  const size = ((v as string)?.length ?? 0) + (format?.length ?? 0) + ((timezoneOffset as string)?.length ?? 0)\n  this.context.memoryLimit.use(size)\n  ...\n  return strftime(date, format)\n}\n```\n\n`strftime` (`src/util/strftime.ts:121`) then walks the format with `rFormat = /%([-_0^#:]+)?(\\d+)?([EO])?(.)/`. The captured `width` group is passed directly to `padStart`:\n\n```ts\nfunction format (d, match) {\n  const [input, flagStr = '', width, modifier, conversion] = match\n  ...\n  let padWidth = width || padWidths[conversion] || 0\n  ...\n  return padStart(ret, padWidth, padChar)   // strftime.ts:147\n}\n```\n\n`padStart` calls `pad()` in `src/util/underscore.ts:153`:\n\n```ts\nexport function pad (str, length, ch, add) {\n  str = String(str)\n  let n = length - str.length\n  while (n-- > 0) str = add(str, ch)   // unbounded loop\n  return str\n}\n```\n\nThe 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.\n\nFilter 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.\n\nThis 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`).\n\n## PoC\n\nSetup: `npm install liquidjs@10.25.7`.\n\nStep 1 — bypass `memoryLimit` and `renderLimit` (5 MB output, ~200 ms, both limits set to 50):\n\n```bash\nnode -e \"\nconst { Liquid } = require('liquidjs');\nconst liquid = new Liquid({ memoryLimit: 50, renderLimit: 50 });\nconst t0 = Date.now();\nconst out = liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%5000000d' });\nconsole.log('len=', out.length, 'ms=', Date.now()-t0);\n\"\n```\n\nVerified output: `len= 5000000 ms= 198`. The `memoryLimit:50` (50-byte budget) and `renderLimit:50` (50 ms budget) are both ignored.\n\nStep 2 — OOM-kill the Node process under a 200 MB heap cap:\n\n```bash\nnode --max-old-space-size=200 -e \"\nconst { Liquid } = require('liquidjs');\nconst liquid = new Liquid({ memoryLimit: 50, renderLimit: 50 });\nliquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%99999999d' });\n\"\n```\n\nVerified output: `FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory`. Process is killed.\n\nThe 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.).\n\n## Impact\n\n- 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.\n- 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.\n- 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`.\n\n## Recommended Fix\n\nTwo complementary fixes:\n\n1. 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:\n\n```ts\nexport function pad (str: any, length: number, ch: string, add: (str: string, ch: string) => string) {\n  str = String(str)\n  const n = length - str.length\n  if (n <= 0) return str\n  return add === ((s, c) => c + s)\n    ? ch.repeat(n) + str\n    : str + ch.repeat(n)\n}\n```\n\n2. 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.\n\nBoth fixes are needed: the cap stops the OOM crash, the memory accounting restores the documented DoS guarantee.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "liquidjs"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "10.25.7"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/harttle/liquidjs/security/advisories/GHSA-hh27-hf48-9f5q"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-45357"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/harttle/liquidjs/commit/3129d46dc95efa357b00e5a57ee1af80a13d72ed"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/harttle/liquidjs"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/harttle/liquidjs/releases/tag/v10.26.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-400" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-27T17:33:52Z",
    "nvd_published_at" : "2026-06-17T23:17:03Z"
  }
}