CVE-2026-61556
LiquidJS: An infinite loop vulnerability in `strip_html` filter
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. From 10.26.0 until 10.27.1, the strip_html filter in src/filters/html.ts can enter an infinite loop when an input string contains <, includes at least one preceding character, and has no later >. In strip_html, the search for the next opener advances lt while the loop index remains unchanged when the closer search returns -1, and the equality-only stall guard does not exit because the loop index is less than lt. Reprocessing the same state indefinitely blocks template rendering and can cause denial of service with an input as short as a<. This issue is fixed in version 10.27.1.
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
8.7
LiquidJS: An infinite loop vulnerability in `strip_html` filter
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. From 10.26.0 until 10.27.1, the strip_html filter in src/filters/html.ts can enter an infinite loop when an input string contains <, includes at least one preceding character, and has no later >. In strip_html, the search for the next opener advances lt while the loop index remains unchanged when the closer search returns -1, and the equality-only stall guard does not exit because the loop index is less than lt. Reprocessing the same state indefinitely blocks template rendering and can cause denial of service with an input as short as a<. This issue is fixed in version 10.27.1.
- Record
CVE-2026-61556- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N - Weakness
- CWE-835
- Updated
- 2026-11-02
EUVDEUVD-2026-63089
8.7
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. From 10.26.0 until 10.27.1, the strip_html filter in src/filters/html.ts can enter an infinite loop when an input string contains <, includes at least one preceding character, and has no later >. In strip_html, the search for the next opener advances lt while the loop index remains unchanged when the closer search returns -1, and the equality-only stall guard does not exit because the loop index is less than lt. Reprocessing the same state indefinitely blocks template rendering and can cause denial of service with an input as short as a<. This issue is fixed in version 10.27.1.
- Record
EUVD-2026-63089- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-m7fp-h3p4-hr49
8.7
LiquidJS has an infinite loop vulnerability in its `strip_html` filter
Summary
The current implementation of strip_html can cause an infinite loop when the input string contains <, has at least one character before <, and no > appears after <.
Details
The problem is in src/filters/html.ts.
Specifically, the following part has the infinite loop.
// Raw-text blocks (HTML5) plus '<...>' as the catch-all kind; a regex
// equivalent is O(n^2) in V8 on unclosed openers.
export function strip_html (this: FilterImpl, v: string) {
const str = stringify(v)
this.context.memoryLimit.use(str.length)
const blocks = new Map([['<script', '</script>'], ['<style', '</style>'], ['<!--', '-->'], ['<', '>']])
let out = ''
let i = 0
while (i < str.length) {
const lt = str.indexOf('<', i)
if (lt < 0) return out + str.slice(i)
out += str.slice(i, lt)
for (const [opener, closer] of blocks) {
if (!str.startsWith(opener, lt)) continue
const e = str.indexOf(closer, lt + opener.length)
if (e >= 0) { i = e + closer.length; break }
blocks.delete(opener)
}
if (i === lt) return out + str.slice(lt)
}
return out
}
For the input "a<", the variable lt is updated to 1 by const lt = str.indexOf('<', i). However, the variable i is never updated from its initial value of 0. This is because in const e = str.indexOf(closer, lt + opener.length), e becomes -1, since there is no > after <. Therefore, when execution reaches if (i === lt) return out + str.slice(lt), i is 0. This is the same state as at the beginning of the loop. As a result, the same thing is repeated again from that state, causing an infinite loop.
PoC
const { Liquid } = require('liquidjs');
const engine = new Liquid();
engine.parseAndRender('{{ html | strip_html }}', {
html: 'a<'
}).then(console.log);
console.log("This is never displayed.");
Impact
This is an infinite loop vulnerability (cf. https://cwe.mitre.org/data/definitions/835.html). This results in a denial of service (DoS). Although a ReDoS vulnerability has previously been reported in the affected function (cf. https://github.com/harttle/liquidjs/security/advisories/GHSA-r7g9-xpmj-5fcq), this issue can cause a more severe impact than that ReDoS vulnerability with an input of only two characters at minimum.
Recommended Fix
There is an issue with the following conditional branch.
if (i === lt) return out + str.slice(lt);
The following should fix the issue.
if (i <= lt) return out + str.slice(lt);
- Record
GHSA-m7fp-h3p4-hr49- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N - Weakness
- CWE-835
- Updated
- 2026-11-02
NVD
8.7
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. From 10.26.0 until 10.27.1, the strip_html filter in src/filters/html.ts can enter an infinite loop when an input string contains <, includes at least one preceding character, and has no later >. In strip_html, the search for the next opener advances lt while the loop index remains unchanged when the closer search returns -1, and the equality-only stall guard does not exit because the loop index is less than lt. Reprocessing the same state indefinitely blocks template rendering and can cause denial of service with an input as short as a<. This issue is fixed in version 10.27.1.
- Record
CVE-2026-61556- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Weakness
- CWE-835
- Updated
- 2026-11-02
OSV
8.7
LiquidJS: An infinite loop vulnerability in `strip_html` filter
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. From 10.26.0 until 10.27.1, the strip_html filter in src/filters/html.ts can enter an infinite loop when an input string contains <, includes at least one preceding character, and has no later >. In strip_html, the search for the next opener advances lt while the loop index remains unchanged when the closer search returns -1, and the equality-only stall guard does not exit because the loop index is less than lt. Reprocessing the same state indefinitely blocks template rendering and can cause denial of service with an input as short as a<. This issue is fixed in version 10.27.1.
- Record
CVE-2026-61556- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N - Weakness
- CWE-835
- Updated
- 2026-11-02
OSVGHSA-m7fp-h3p4-hr49
8.7
LiquidJS has an infinite loop vulnerability in its `strip_html` filter
Summary
The current implementation of strip_html can cause an infinite loop when the input string contains <, has at least one character before <, and no > appears after <.
Details
The problem is in src/filters/html.ts.
Specifically, the following part has the infinite loop.
// Raw-text blocks (HTML5) plus '<...>' as the catch-all kind; a regex
// equivalent is O(n^2) in V8 on unclosed openers.
export function strip_html (this: FilterImpl, v: string) {
const str = stringify(v)
this.context.memoryLimit.use(str.length)
const blocks = new Map([['<script', '</script>'], ['<style', '</style>'], ['<!--', '-->'], ['<', '>']])
let out = ''
let i = 0
while (i < str.length) {
const lt = str.indexOf('<', i)
if (lt < 0) return out + str.slice(i)
out += str.slice(i, lt)
for (const [opener, closer] of blocks) {
if (!str.startsWith(opener, lt)) continue
const e = str.indexOf(closer, lt + opener.length)
if (e >= 0) { i = e + closer.length; break }
blocks.delete(opener)
}
if (i === lt) return out + str.slice(lt)
}
return out
}
For the input "a<", the variable lt is updated to 1 by const lt = str.indexOf('<', i). However, the variable i is never updated from its initial value of 0. This is because in const e = str.indexOf(closer, lt + opener.length), e becomes -1, since there is no > after <. Therefore, when execution reaches if (i === lt) return out + str.slice(lt), i is 0. This is the same state as at the beginning of the loop. As a result, the same thing is repeated again from that state, causing an infinite loop.
PoC
const { Liquid } = require('liquidjs');
const engine = new Liquid();
engine.parseAndRender('{{ html | strip_html }}', {
html: 'a<'
}).then(console.log);
console.log("This is never displayed.");
Impact
This is an infinite loop vulnerability (cf. https://cwe.mitre.org/data/definitions/835.html). This results in a denial of service (DoS). Although a ReDoS vulnerability has previously been reported in the affected function (cf. https://github.com/harttle/liquidjs/security/advisories/GHSA-r7g9-xpmj-5fcq), this issue can cause a more severe impact than that ReDoS vulnerability with an input of only two characters at minimum.
Recommended Fix
There is an issue with the following conditional branch.
if (i === lt) return out + str.slice(lt);
The following should fix the issue.
if (i <= lt) return out + str.slice(lt);
- Record
GHSA-m7fp-h3p4-hr49- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N - Weakness
- CWE-835
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
liquidjsnpm |
10.26.0 up to 10.27.1 | 10.27.1 |
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-61556 is a high vulnerability (CVSS 8.7) in Harttle Liquidjs, published August 19, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.52%, the 44th percentile. Fixed in liquidjs 10.27.1.
References
github.com/harttle/liquidjs/security/advisories/GHSA-m7fp-h3p4-hr49
github.com/harttle/liquidjs/pull/917
github.com/harttle/liquidjs/commit/5c3522f33928aae66f0fe85c36e1d9015c768fe2
github.com/harttle/liquidjs/releases/tag/v10.27.1
nvd.nist.gov/vuln/detail/CVE-2026-61556
github.com/advisories/GHSA-m7fp-h3p4-hr49
github.com/CVEProject/cvelistV5/tree/main/cves/2026/61xxx/CVE-2026-61556.json
github.com/harttle/liquidjs