GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-M7FP-H3P4-HR49

Vulnerability from github – Published: 2026-09-03 17:45 – Updated: 2026-09-03 17:45
VLAI
Summary
LiquidJS has an infinite loop vulnerability in its `strip_html` filter
Details

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);
Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…