highCVE-2026-61556Sep 3, 2026

CVE-2026-61556: LiquidJS strip_html Infinite Loop (DoS)

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A two-character input to LiquidJS's built-in strip_html filter causes the Node.js event loop to spin forever, making any app that renders untrusted content permanently unresponsive.

Packageliquidjs
Ecosystemnpm
Affected>= 10.26.0, < 10.27.1
Fixed in10.27.1
CVE-2026-61556: LiquidJS strip_html Infinite Loop (DoS)

The problem

The strip_html filter in LiquidJS (versions 10.26.0 through 10.27.0) rewrites its HTML-stripping logic as a manual single-pass scanner to avoid a prior ReDoS. That scanner contains an off-by-one in its loop-exit guard.

When input contains a < with at least one character before it and no closing > anywhere after it, the loop variable i is never advanced. The check if (i === lt) only exits the loop when i and lt start at the same position, which does not happen for input like "a<".

The loop restarts from the same state indefinitely, hanging the process and enabling DoS with just two characters of input.

Proof of concept

A working proof-of-concept for CVE-2026-61556 in liquidjs, with the exact payload below.

javascript
const { Liquid } = require('liquidjs');

const engine = new Liquid();

// Two characters are enough to hang the process forever.
engine.parseAndRender('{{ html | strip_html }}', {
  html: 'a<'
}).then(console.log);

console.log('This line is never reached.');

The root cause is a wrong equality check in the loop-exit guard inside src/filters/html.ts. After the inner for loop exhausts all openers without finding a closer, execution falls through to if (i === lt). For input "a<", i is 0 and lt is 1, so the condition is false and the loop does not exit.

The outer while loop restarts with i still at 0, finds lt = 1 again, and repeats forever (CWE-835).

The one-character patch changes === to <=. This makes the guard fire whenever i has not advanced past lt, which covers the off-by-one case and all similar stuck states, correctly returning the remaining unprocessed input instead of looping.

The fix

Upgrade to liquidjs 10.27.1. The patch changes a single operator in src/filters/html.ts: the loop-exit check if (i === lt) becomes if (i <= lt), ensuring the function always returns when no closer is found after an opener. No configuration workaround exists for older versions; upgrading is the only remediation.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research