Public mode

No access to your tenant or devices. Every item links to its source. What this means

Some sources, documents or advisories need attention: 272/272 feeds/APIs · 469/469 docs · 7/9 advisories · checked 15:55 UTC Customize Public modeDiscuss ChangeIntel on Discord

LiquidJS has a renderLimit DoS guard bypass via empty `{% for %}` body

Collected exploitation and prediction evidence

Risk source coverage

CISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.

No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.

cve-program · CVE-2026-44645

Published

LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the renderLimit option can be fully bypassed by a {% for %} (or {% tablerow %}) tag whose body is empty. The renderLimit option is documented in docs/source/tutorials/dos.md as the mechanism that "mitigates this by limiting the time consumed by each render() call." The per-iteration time check is reached only when the body contains at least one template node, so a template such as {%- for i in (1..N) -%}{%- endfor -%} iterates the full collection without ever consulting renderLimit. With a configured renderLimit of 50 ms, a single parseAndRenderSync call has been observed to consume 2.26 seconds (~45× over the limit) and scales linearly with N up to memoryLimit, allowing a low-privileged template author to wedge an event-loop thread for an attacker-chosen duration. Deployments that rely on a finite renderLimit for DoS protection (common in multi-tenant template-authoring environments) can still be forced by a single crafted template to monopolize a Node.js event-loop worker for attacker-controlled time, potentially stalling in-flight requests, with availability impact only. This issue has been fixed in version 10.26.0.

Source published
17 Jun 2026 22:08:19 UTC
Source modified
18 Jun 2026 19:05:32 UTC
Observed here
23 Oct 2026 15:48:27 UTC
Source terms
CVE Program Terms of Use

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.

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

euvd · EUVD-2026-37822

Published

LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the renderLimit option can be fully bypassed by a {% for %} (or {% tablerow %}) tag whose body is empty. The renderLimit option is documented in docs/source/tutorials/dos.md as the mechanism that "mitigates this by limiting the time consumed by each render() call." The per-iteration time check is reached only when the body contains at least one template node, so a template such as {%- for i in (1..N) -%}{%- endfor -%} iterates the full collection without ever consulting renderLimit. With a configured renderLimit of 50 ms, a single parseAndRenderSync call has been observed to consume 2.26 seconds (~45× over the limit) and scales linearly with N up to memoryLimit, allowing a low-privileged template author to wedge an event-loop thread for an attacker-chosen duration. Deployments that rely on a finite renderLimit for DoS protection (common in multi-tenant template-authoring environments) can still be forced by a single crafted template to monopolize a Node.js event-loop worker for attacker-controlled time, potentially stalling in-flight requests, with availability impact only. This issue has been fixed in version 10.26.0.

EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.

Original publisher date fields

datePublished: Jun 17, 2026, 10:08:19 PM

dateUpdated: Jun 18, 2026, 7:05:32 PM

Source published
Jun 17, 2026, 10:08:19 PM (timezone not stated)
Source modified
Jun 18, 2026, 7:05:32 PM (timezone not stated)
Observed here
30 Oct 2026 07:53:36 UTC
Source terms
ENISA EUVD · source terms and attribution apply

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: euvd · ENISA EUVD · source terms and attribution apply. Source text is retained; field grouping and change labels are provided by ChangeIntel.

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

nvd · CVE-2026-44645

Published

LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the renderLimit option can be fully bypassed by a {% for %} (or {% tablerow %}) tag whose body is empty. The renderLimit option is documented in docs/source/tutorials/dos.md as the mechanism that "mitigates this by limiting the time consumed by each render() call." The per-iteration time check is reached only when the body contains at least one template node, so a template such as {%- for i in (1..N) -%}{%- endfor -%} iterates the full collection without ever consulting renderLimit. With a configured renderLimit of 50 ms, a single parseAndRenderSync call has been observed to consume 2.26 seconds (~45× over the limit) and scales linearly with N up to memoryLimit, allowing a low-privileged template author to wedge an event-loop thread for an attacker-chosen duration. Deployments that rely on a finite renderLimit for DoS protection (common in multi-tenant template-authoring environments) can still be forced by a single crafted template to monopolize a Node.js event-loop worker for attacker-controlled time, potentially stalling in-flight requests, with availability impact only. This issue has been fixed in version 10.26.0.

NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.

Source analysis status: Deferred

Weakness classifications: CWE-400

Original publisher date fields

published: 2026-06-17T23:17:03.607

lastModified: 2026-10-25T23:10:00.167

Source published
17 Jun 2026 23:17:03 UTC
Source modified
25 Oct 2026 23:10:00 UTC
Observed here
27 Oct 2026 11:12:17 UTC
Source terms
NIST NVD usage terms; underlying source attribution retained

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 3.1 · [email protected] · Secondary · 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

osv · GHSA-8xx9-69p8-7jp3

Published

## Summary The `renderLimit` option — documented in `docs/source/tutorials/dos.md` as the mechanism that "mitigates this by limiting the time consumed by each render() call" — can be fully bypassed by a `{% for %}` (or `{% tablerow %}`) tag whose body is empty. The per-iteration time check is reached only when the body contains at least one template node, so a template like `{%- for i in (1..N) -%}{%- endfor -%}` iterates the full collection without ever consulting `renderLimit`. With a configured `renderLimit` of 50 ms, a single `parseAndRenderSync` call has been observed to consume **2.26 seconds** (~45× over the limit) and scales linearly with `N` up to `memoryLimit`, allowing a low-privileged template author to wedge an event-loop thread for an attacker-chosen duration. ## Details `Render.renderTemplates` is the single point at which `renderLimit` is consulted: ```ts // src/render/render.ts 14: public * renderTemplates (templates: Template[], ctx: Context, emitter?: Emitter): IterableIterator<any> { 15: if (!emitter) { 16: emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter() : new SimpleEmitter() 17: } 18: const errors = [] 19: for (const tpl of templates) { 20: ctx.renderLimit.check(getPerformance().now()) 21: try { 22: const html = yield tpl.render(ctx, emitter) ... 32: } ``` The check at line 20 lives **inside** the `for (const tpl of templates)` body. When `templates.length === 0`, the loop body never executes, so the limiter is never consulted on that invocation. The `for` tag re-enters `renderTemplates` once per collection item with no independent time check: ```ts // src/tags/for.ts 70: for (const item of collection) { 71: scope[this.variable] = item 72: ctx.continueCalled = ctx.breakCalled = false 73: yield r.renderTemplates(this.templates, ctx, emitter) 74: if (ctx.breakCalled) break 75: scope.forloop.next() 76: } ``` When `{%- for i in (1..N) -%}{%- endfor -%}` is parsed, `this.templates` is `[]`. Each of the `N` calls to `r.renderTemplates(this.templates, ctx, emitter)` therefore performs zero `renderLimit.check()` calls and zero template work — it just spins the JS-level `for` loop and the generator boilerplate. With `N = 30_000_000` this still costs ~2.26 s of CPU, and `N = 100_000_000` costs ~9.6 s, fully bypassing whatever wall-clock budget the integrator configured. The range expression itself is bounded only by `memoryLimit`: ```ts // src/render/expression.ts:67-72 function * evalRangeToken (token: RangeToken, ctx: Context) { const low: number = yield evalToken(token.lhs, ctx) const high: number = yield evalToken(token.rhs, ctx) ctx.memoryLimit.use(high - low + 1) return range(+low, +high + 1) } ``` So the maximum bypass is governed by the (separate) `memoryLimit`, not by `renderLimit`. Integrators following the `docs/source/tutorials/dos.md` guidance — which positions `renderLimit` as the time-based defense — get no time-based defense at all on this code path. ## PoC Reproduced against `[email protected]` (HEAD `34877950`): ```bash # Empty for-body bypasses renderLimit (50 ms) and runs for ~2.26 s: $ node -e "const { Liquid } = require('liquidjs'); const engine = new Liquid({ memoryLimit: 1e9, renderLimit: 50 }); const t = Date.now(); engine.parseAndRenderSync('{%- for i in (1..30000000) -%}{%- endfor -%}', {}); console.log('Took', Date.now()-t, 'ms');" Took 2255 ms # Same template with a single-character body is correctly bounded: $ node -e "const { Liquid } = require('liquidjs'); const engine = new Liquid({ memoryLimit: 1e9, renderLimit: 50 }); try { engine.parseAndRenderSync('{%- for i in (1..30000000) -%}.{%- endfor -%}', {}); } catch(e) { console.log('correctly threw:', e.message); }" correctly threw: template render limit exceeded, line:1, col:1 ``` Scaling `N`: - `N = 30_000_000` → 2255 ms (≈ 45× over the 50 ms limit) - `N = 100_000_000` → 9581 ms (≈ 191× over the 50 ms limit) Time grows linearly with `N`, capped only by `memoryLimit` (default `Infinity`, so the only cap by default is process memory). ## Impact Any liquidjs integrator who follows the upstream DoS guidance and sets a finite `renderLimit` to bound per-render CPU — typical for SaaS / multi-tenant environments where end users author templates (themes, email templates, snippets) — does not get the bound they configured. A single template submission can keep an event-loop thread busy for seconds, which on a Node.js server is sufficient to stall all in-flight requests on that worker. With a large enough range and a permissive `memoryLimit`, the wedge time is attacker-controlled. No data is exposed and no integrity is harmed; impact is availability only. ## Recommended Fix Move the `renderLimit` check to a location that runs unconditionally per `renderTemplates` invocation, so a zero-template body still triggers it; alternatively (or additionally) have iteration tags that invoke `renderTemplates` per element check the limiter themselves once per iteration. ```ts // src/render/render.ts — check at function entry, before the templates loop public * renderTemplates (templates: Template[], ctx: Context, emitter?: Emitter): IterableIterator<any> { if (!emitter) { emitter = ctx.opts.keepOutputType ? new KeepingTypeEmitter() : new SimpleEmitter() } ctx.renderLimit.check(getPerformance().now()) // <-- runs even when templates is empty const errors = [] for (const tpl of templates) { ctx.renderLimit.check(getPerformance().now()) ... } ... } ``` And/or, defensively, in the iteration tags themselves so the guard cost is paid once per element rather than only at re-entry: ```ts // src/tags/for.ts (around line 70) for (const item of collection) { ctx.renderLimit.check(getPerformance().now()) // <-- per-iteration time check scope[this.variable] = item ctx.continueCalled = ctx.breakCalled = false yield r.renderTemplates(this.templates, ctx, emitter) if (ctx.breakCalled) break scope.forloop.next() } // src/tags/tablerow.ts (around line 54) — analogous addition for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) { ctx.renderLimit.check(getPerformance().now()) ... } ``` The same hardening should be applied anywhere a tag drives an attacker-influenced loop count over a (potentially empty) `templates` array.

Source published
27 May 2026 00:11:46 UTC
Source modified
27 Oct 2026 15:15:08 UTC
Observed here
27 Oct 2026 15:44:25 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.

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

Publisher rating · MODERATE

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 30 Oct 15:55 UTC · 272 of 272 readable · 271 fully read · documentation 469/469 current