{"id":"GHSA-m7fp-h3p4-hr49","summary":"LiquidJS has an infinite loop vulnerability in its `strip_html` filter","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 '\u003c...\u003e' 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([['\u003cscript', '\u003c/script\u003e'], ['\u003cstyle', '\u003c/style\u003e'], ['\u003c!--', '--\u003e'], ['\u003c', '\u003e']])\n  let out = ''\n  let i = 0\n  while (i \u003c str.length) {\n    const lt = str.indexOf('\u003c', 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('\u003c', 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('liquidjs');\n\nconst engine = new Liquid();\n\nengine.parseAndRender('{{ html | strip_html }}', {\n  html: 'a\u003c'\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```","aliases":["CVE-2026-61556"],"modified":"2026-09-03T18:00:04.447368207Z","published":"2026-09-03T17:45:26Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-09-03T17:45:26Z","nvd_published_at":"2026-08-19T21:17:03Z","cwe_ids":["CWE-835"]},"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"}],"affected":[{"package":{"name":"liquidjs","ecosystem":"npm","purl":"pkg:npm/liquidjs"},"ranges":[{"type":"SEMVER","events":[{"introduced":"10.26.0"},{"fixed":"10.27.1"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-m7fp-h3p4-hr49/GHSA-m7fp-h3p4-hr49.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","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"}]}