{"id":"CVE-2026-61556","aliases":["EUVD-2026-63089","GHSA-m7fp-h3p4-hr49"],"status":"published","title":"LiquidJS: An infinite loop vulnerability in `strip_html` filter","description":"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 \u003c, includes at least one preceding character, and has no later \u003e. 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\u003c. This issue is fixed in version 10.27.1.","severity":{"version":"4.0","score":8.7,"vector":"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","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00524,"percentile":0.44762,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-835"],"published":"2026-08-19T20:49:56Z","modified":"2026-10-28T02:30:27Z","fixes":[{"ecosystem":"npm","package":"liquidjs","fixed_in":["10.27.1"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"liquidjs","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"10.26.0"},{"fixed":"10.27.1"}]}]},{"source":"osv","ecosystem":"npm","package":"liquidjs","ranges":[{"type":"SEMVER","events":[{"introduced":"10.26.0"},{"fixed":"10.27.1"}]}]}],"products":[{"vendor":"harttle","product":"liquidjs"}],"references":[{"url":"https://github.com/harttle/liquidjs/security/advisories/GHSA-m7fp-h3p4-hr49","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/harttle/liquidjs/pull/917","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/harttle/liquidjs/commit/5c3522f33928aae66f0fe85c36e1d9015c768fe2","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/harttle/liquidjs/releases/tag/v10.27.1","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-61556","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-m7fp-h3p4-hr49"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/61xxx/CVE-2026-61556.json","tags":["advisory"]},{"url":"https://github.com/harttle/liquidjs","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-61556","title":"LiquidJS: An infinite loop vulnerability in `strip_html` filter","description":"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 \u003c, includes at least one preceding character, and has no later \u003e. 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\u003c. This issue is fixed in version 10.27.1.","status":"published","published":"2026-08-19T20:49:56.02Z","modified":"2026-08-25T14:16:38.259Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high","assessor":"cna"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T14:19:53.4444Z"}],"euvd":[{"id":"EUVD-2026-63089","description":"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 \u003c, includes at least one preceding character, and has no later \u003e. 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\u003c. This issue is fixed in version 10.27.1.","status":"published","published":"2026-08-19T20:49:56Z","modified":"2026-08-25T14:16:38Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high"}],"updated_at":"2026-11-02T18:01:33.385611Z"}],"ghsa":[{"id":"GHSA-m7fp-h3p4-hr49","title":"LiquidJS has an infinite loop vulnerability in its `strip_html` filter","description":"### 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```","status":"published","published":"2026-09-03T17:45:26Z","modified":"2026-09-03T17:45:28Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T14:41:11.621873Z"}],"nvd":[{"id":"CVE-2026-61556","description":"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 \u003c, includes at least one preceding character, and has no later \u003e. 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\u003c. This issue is fixed in version 10.27.1.","status":"published","published":"2026-08-19T21:17:03.633Z","modified":"2026-09-09T21:19:49.197Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high","assessor":"secondary"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T14:12:39.962615Z"}],"osv":[{"id":"CVE-2026-61556","title":"LiquidJS: An infinite loop vulnerability in `strip_html` filter","description":"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 \u003c, includes at least one preceding character, and has no later \u003e. 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\u003c. This issue is fixed in version 10.27.1.","status":"published","published":"2026-08-19T20:49:56.02Z","modified":"2026-10-28T02:30:27.668980236Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T15:45:12.933503Z"},{"id":"GHSA-m7fp-h3p4-hr49","title":"LiquidJS has an infinite loop vulnerability in its `strip_html` filter","description":"### 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```","status":"published","published":"2026-09-03T17:45:26Z","modified":"2026-09-03T18:00:04.447368207Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T15:46:51.463834Z"}]}}
