GHSA-M7FP-H3P4-HR49
Vulnerability from github – Published: 2026-09-03 17:45 – Updated: 2026-09-03 17:45Summary
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);
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "liquidjs"
},
"ranges": [
{
"events": [
{
"introduced": "10.26.0"
},
{
"fixed": "10.27.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-61556"
],
"database_specific": {
"cwe_ids": [
"CWE-835"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-03T17:45:26Z",
"nvd_published_at": "2026-08-19T21:17:03Z",
"severity": "HIGH"
},
"details": "### Summary\nThe current implementation of `strip_html` can cause an infinite loop when the input string contains `\u003c`, has at least one character before `\u003c`, and no `\u003e` appears after `\u003c`.\n\n### Details\nThe problem is in `src/filters/html.ts`.\nSpecifically, the following part has the infinite loop.\n\n```\n// Raw-text blocks (HTML5) plus \u0027\u003c...\u003e\u0027 as the catch-all kind; a regex\n// equivalent is O(n^2) in V8 on unclosed openers.\nexport function strip_html (this: FilterImpl, v: string) {\n const str = stringify(v)\n this.context.memoryLimit.use(str.length)\n const blocks = new Map([[\u0027\u003cscript\u0027, \u0027\u003c/script\u003e\u0027], [\u0027\u003cstyle\u0027, \u0027\u003c/style\u003e\u0027], [\u0027\u003c!--\u0027, \u0027--\u003e\u0027], [\u0027\u003c\u0027, \u0027\u003e\u0027]])\n let out = \u0027\u0027\n let i = 0\n while (i \u003c str.length) {\n const lt = str.indexOf(\u0027\u003c\u0027, i)\n if (lt \u003c 0) return out + str.slice(i)\n out += str.slice(i, lt)\n for (const [opener, closer] of blocks) {\n if (!str.startsWith(opener, lt)) continue\n const e = str.indexOf(closer, lt + opener.length)\n if (e \u003e= 0) { i = e + closer.length; break }\n blocks.delete(opener)\n }\n if (i === lt) return out + str.slice(lt)\n }\n return out\n}\n```\n\nFor the input \"a\u003c\", the variable `lt` is updated to 1 by `const lt = str.indexOf(\u0027\u003c\u0027, 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 \u003e after \u003c. 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.\n\n\n### PoC\n```\nconst { Liquid } = require(\u0027liquidjs\u0027);\n\nconst engine = new Liquid();\n\nengine.parseAndRender(\u0027{{ html | strip_html }}\u0027, {\n html: \u0027a\u003c\u0027\n}).then(console.log);\n\nconsole.log(\"This is never displayed.\");\n```\n\n### Impact\nThis 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.\n\n### Recommended Fix\nThere is an issue with the following conditional branch.\n\n```\nif (i === lt) return out + str.slice(lt);\n```\n\nThe following should fix the issue.\n\n```\nif (i \u003c= lt) return out + str.slice(lt);\n```",
"id": "GHSA-m7fp-h3p4-hr49",
"modified": "2026-09-03T17:45:26Z",
"published": "2026-09-03T17:45:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/harttle/liquidjs/security/advisories/GHSA-m7fp-h3p4-hr49"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-61556"
},
{
"type": "WEB",
"url": "https://github.com/harttle/liquidjs/pull/917"
},
{
"type": "WEB",
"url": "https://github.com/harttle/liquidjs/commit/5c3522f33928aae66f0fe85c36e1d9015c768fe2"
},
{
"type": "PACKAGE",
"url": "https://github.com/harttle/liquidjs"
},
{
"type": "WEB",
"url": "https://github.com/harttle/liquidjs/releases/tag/v10.27.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "LiquidJS has an infinite loop vulnerability in its `strip_html` filter"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.