CVE-2026-61556: LiquidJS: An infinite loop vulnerability in `strip_html` filter
Summary The current implementation of striphtml 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 striphtml (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 | striphtml }}', { 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);
Other sources
LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. From 10.26.0 until 10.27.1, the striphtml 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 striphtml, 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.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/liquidjsto a version that resolves this vulnerability.Fixed in 10.27.1 - Upgrade
Upgrade
liquidjsto a version that resolves this vulnerability.Fixed in 10.27.1 - Compensating control
Mitigate the DoS risk by ensuring template rendering inputs are validated/limited so that strings containing '<' with no subsequent '>' (e.g., input like 'a<') are rejected or constrained before LiquidJS processes them.
Event History
Frequently Asked Questions
What inputs can trigger the denial of service?
A string processed by the strip_html filter can trigger the loop if it contains a '<' character with at least one character before it and no later '>' character. The described proof-of-concept input is "a<".
Who can realistically exploit this issue?
An attacker would need to cause attacker-controlled or otherwise malformed text matching the trigger condition to be processed by LiquidJS's strip_html filter. Successful exploitation blocks template rendering through an infinite loop.
Which versions need remediation?
The issue affects LiquidJS versions from 10.26.0 until 10.27.1. Version 10.27.1 fixes the vulnerability.