CVE-2026-44644
LiquidJS's strip_html filter bypass via newline characters in HTML tags enables XSS
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. Versions 10.25.7 and below are vulnerable to XSS through a flaw in the strip_html filter logic. The strip_html filter is intended to remove HTML tags from a string before rendering, and is widely used as an XSS sanitizer. The implementation uses a regex whose catch-all branch (<.*?>) does not match line terminators, so any HTML tag containing a \n or \r character passes through unmodified. An attacker who can place a newline inside a tag (e.g. <img\nsrc=x\nonerror=alert(1)>) bypasses sanitization entirely, since browsers treat newlines as whitespace within a tag and execute the resulting onerror/onload/etc. handler. Exploitation is possible for applications that both render attacker-controlled strings via {{ x | strip_html }} to defend against HTML injection and do not separately HTML-escape that output (default behavior — outputEscape is unset by default). This issue has been fixed in version 10.26.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- Required
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
6.1
LiquidJS's strip_html filter bypass via newline characters in HTML tags enables XSS
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. Versions 10.25.7 and below are vulnerable to XSS through a flaw in the strip_html filter logic. The strip_html filter is intended to remove HTML tags from a string before rendering, and is widely used as an XSS sanitizer. The implementation uses a regex whose catch-all branch (<.*?>) does not match line terminators, so any HTML tag containing a \n or \r character passes through unmodified. An attacker who can place a newline inside a tag (e.g. <img\nsrc=x\nonerror=alert(1)>) bypasses sanitization entirely, since browsers treat newlines as whitespace within a tag and execute the resulting onerror/onload/etc. handler. Exploitation is possible for applications that both render attacker-controlled strings via {{ x | strip_html }} to defend against HTML injection and do not separately HTML-escape that output (default behavior — outputEscape is unset by default). This issue has been fixed in version 10.26.0.
- Record
CVE-2026-44644- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
EUVDEUVD-2026-37815
6.1
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. Versions 10.25.7 and below are vulnerable to XSS through a flaw in the strip_html filter logic. The strip_html filter is intended to remove HTML tags from a string before rendering, and is widely used as an XSS sanitizer. The implementation uses a regex whose catch-all branch (<.*?>) does not match line terminators, so any HTML tag containing a \n or \r character passes through unmodified. An attacker who can place a newline inside a tag (e.g. <img\nsrc=x\nonerror=alert(1)>) bypasses sanitization entirely, since browsers treat newlines as whitespace within a tag and execute the resulting onerror/onload/etc. handler. Exploitation is possible for applications that both render attacker-controlled strings via {{ x | strip_html }} to defend against HTML injection and do not separately HTML-escape that output (default behavior — outputEscape is unset by default). This issue has been fixed in version 10.26.0.
- Record
EUVD-2026-37815- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-2qv6-9wx5-cwv4
6.1
LiquidJS's strip_html filter bypass via newline characters in HTML tags enables XSS
Summary
The strip_html filter in liquidjs is intended to remove HTML tags from a string before rendering, and is widely used as an XSS sanitizer. The implementation uses a regex whose catch-all branch (<.*?>) does not match line terminators, so any HTML tag containing a \n or \r character passes through unmodified. An attacker who can place a newline inside a tag (e.g. <img\nsrc=x\nonerror=alert(1)>) bypasses sanitization entirely, since browsers treat newlines as whitespace within a tag and execute the resulting onerror/onload/etc. handler. This results in stored or reflected XSS in any application that relies on strip_html to neutralize untrusted HTML.
Details
The vulnerable code is in src/filters/html.ts:
// src/filters/html.ts:45-49
export function strip_html (this: FilterImpl, v: string) {
const str = stringify(v)
this.context.memoryLimit.use(str.length)
return str.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '')
}
The regex has four alternations:
<script[\s\S]*?<\/script>— uses[\s\S], matches across newlines.<style[\s\S]*?<\/style>— uses[\s\S], matches across newlines.<.*?>— uses., which in JavaScript does not match\nor\r(nos/dotAll flag set).<!--[\s\S]*?-->— uses[\s\S], matches across newlines.
Branch 3 is the catch-all for "any other tag." Because . excludes line terminators, a tag containing a newline does not match any alternative. The literal characters of the tag are passed through to the output.
Browsers, however, parse HTML tag content with whitespace tolerance: per the HTML spec, attribute names and values may be separated by ASCII whitespace, which includes \n and \r. So <img\nsrc=x\nonerror=alert(1)> is parsed as a valid img element with an onerror handler.
liquidjs' default rendering pipeline does not auto-escape filter output (the outputEscape engine option is undefined by default — see src/liquid-options.ts), so the unescaped HTML is delivered verbatim to the consumer's HTML response.
Trust path:
- Application receives untrusted input (e.g. user comment field).
- Developer renders it as
{{ comment | strip_html }}to "safely" embed user content as plaintext. - Attacker submits
<img\u000Asrc=x\u000Aonerror=alert(document.cookie)>. strip_htmlreturns the input unchanged.- Output is written into the HTML response with no further escaping.
- Victim's browser executes the attacker's JavaScript in the application's origin.
This is an inconsistency bug: the same regex correctly uses [\s\S] for <script>, <style>, and comment branches, but reverts to . for the catch-all. The other branches' authors clearly knew to handle multi-line content; the catch-all was missed.
PoC
Reproduces against current HEAD (10.25.7) using the published dist/liquid.node.js build:
node -e "
const { Liquid } = require('./dist/liquid.node.js');
const engine = new Liquid();
engine.parseAndRender(
'Safe output: {{ input | strip_html }}',
{ input: '<img\nsrc=x\nonerror=\"alert(document.cookie)\">' }
).then(r => console.log(JSON.stringify(r)));
"
Verified output:
"Safe output: <img\nsrc=x\nonerror=\"alert(document.cookie)\">"
The <img ... onerror=...> tag is delivered to the output completely unmodified. When this string is placed into an HTML document and parsed by a browser, the onerror handler executes.
Same bypass works with \r (carriage return), \r\n, or any combination of CR/LF inside the tag. It also works with other event-handler vectors (<svg\nonload=alert(1)>, <body\nonload=alert(1)>, <iframe\nsrc="javascript:alert(1)">, etc.) and is not specific to <img>.
For comparison, the same input without a newline is correctly stripped:
node -e "
const { Liquid } = require('./dist/liquid.node.js');
const engine = new Liquid();
engine.parseAndRender(
'Safe output: {{ input | strip_html }}',
{…
- Record
GHSA-2qv6-9wx5-cwv4- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
NVD
6.1
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. Versions 10.25.7 and below are vulnerable to XSS through a flaw in the strip_html filter logic. The strip_html filter is intended to remove HTML tags from a string before rendering, and is widely used as an XSS sanitizer. The implementation uses a regex whose catch-all branch (<.*?>) does not match line terminators, so any HTML tag containing a \n or \r character passes through unmodified. An attacker who can place a newline inside a tag (e.g. <img\nsrc=x\nonerror=alert(1)>) bypasses sanitization entirely, since browsers treat newlines as whitespace within a tag and execute the resulting onerror/onload/etc. handler. Exploitation is possible for applications that both render attacker-controlled strings via {{ x | strip_html }} to defend against HTML injection and do not separately HTML-escape that output (default behavior — outputEscape is unset by default). This issue has been fixed in version 10.26.0.
- Record
CVE-2026-44644- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
OSV
6.1
LiquidJS's strip_html filter bypass via newline characters in HTML tags enables XSS
LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. Versions 10.25.7 and below are vulnerable to XSS through a flaw in the strip_html filter logic. The strip_html filter is intended to remove HTML tags from a string before rendering, and is widely used as an XSS sanitizer. The implementation uses a regex whose catch-all branch (<.*?>) does not match line terminators, so any HTML tag containing a \n or \r character passes through unmodified. An attacker who can place a newline inside a tag (e.g. <img\nsrc=x\nonerror=alert(1)>) bypasses sanitization entirely, since browsers treat newlines as whitespace within a tag and execute the resulting onerror/onload/etc. handler. Exploitation is possible for applications that both render attacker-controlled strings via {{ x | strip_html }} to defend against HTML injection and do not separately HTML-escape that output (default behavior — outputEscape is unset by default). This issue has been fixed in version 10.26.0.
- Record
CVE-2026-44644- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
OSVGHSA-2qv6-9wx5-cwv4
6.1
LiquidJS's strip_html filter bypass via newline characters in HTML tags enables XSS
Summary
The strip_html filter in liquidjs is intended to remove HTML tags from a string before rendering, and is widely used as an XSS sanitizer. The implementation uses a regex whose catch-all branch (<.*?>) does not match line terminators, so any HTML tag containing a \n or \r character passes through unmodified. An attacker who can place a newline inside a tag (e.g. <img\nsrc=x\nonerror=alert(1)>) bypasses sanitization entirely, since browsers treat newlines as whitespace within a tag and execute the resulting onerror/onload/etc. handler. This results in stored or reflected XSS in any application that relies on strip_html to neutralize untrusted HTML.
Details
The vulnerable code is in src/filters/html.ts:
// src/filters/html.ts:45-49
export function strip_html (this: FilterImpl, v: string) {
const str = stringify(v)
this.context.memoryLimit.use(str.length)
return str.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<.*?>|<!--[\s\S]*?-->/g, '')
}
The regex has four alternations:
<script[\s\S]*?<\/script>— uses[\s\S], matches across newlines.<style[\s\S]*?<\/style>— uses[\s\S], matches across newlines.<.*?>— uses., which in JavaScript does not match\nor\r(nos/dotAll flag set).<!--[\s\S]*?-->— uses[\s\S], matches across newlines.
Branch 3 is the catch-all for "any other tag." Because . excludes line terminators, a tag containing a newline does not match any alternative. The literal characters of the tag are passed through to the output.
Browsers, however, parse HTML tag content with whitespace tolerance: per the HTML spec, attribute names and values may be separated by ASCII whitespace, which includes \n and \r. So <img\nsrc=x\nonerror=alert(1)> is parsed as a valid img element with an onerror handler.
liquidjs' default rendering pipeline does not auto-escape filter output (the outputEscape engine option is undefined by default — see src/liquid-options.ts), so the unescaped HTML is delivered verbatim to the consumer's HTML response.
Trust path:
- Application receives untrusted input (e.g. user comment field).
- Developer renders it as
{{ comment | strip_html }}to "safely" embed user content as plaintext. - Attacker submits
<img\u000Asrc=x\u000Aonerror=alert(document.cookie)>. strip_htmlreturns the input unchanged.- Output is written into the HTML response with no further escaping.
- Victim's browser executes the attacker's JavaScript in the application's origin.
This is an inconsistency bug: the same regex correctly uses [\s\S] for <script>, <style>, and comment branches, but reverts to . for the catch-all. The other branches' authors clearly knew to handle multi-line content; the catch-all was missed.
PoC
Reproduces against current HEAD (10.25.7) using the published dist/liquid.node.js build:
node -e "
const { Liquid } = require('./dist/liquid.node.js');
const engine = new Liquid();
engine.parseAndRender(
'Safe output: {{ input | strip_html }}',
{ input: '<img\nsrc=x\nonerror=\"alert(document.cookie)\">' }
).then(r => console.log(JSON.stringify(r)));
"
Verified output:
"Safe output: <img\nsrc=x\nonerror=\"alert(document.cookie)\">"
The <img ... onerror=...> tag is delivered to the output completely unmodified. When this string is placed into an HTML document and parsed by a browser, the onerror handler executes.
Same bypass works with \r (carriage return), \r\n, or any combination of CR/LF inside the tag. It also works with other event-handler vectors (<svg\nonload=alert(1)>, <body\nonload=alert(1)>, <iframe\nsrc="javascript:alert(1)">, etc.) and is not specific to <img>.
For comparison, the same input without a newline is correctly stripped:
node -e "
const { Liquid } = require('./dist/liquid.node.js');
const engine = new Liquid();
engine.parseAndRender(
'Safe output: {{ input | strip_html }}',
{…
- Record
GHSA-2qv6-9wx5-cwv4- CVSS 3.1
- 6.1 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
liquidjsnpm |
before 10.26.0 | 10.26.0 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-44644 is a medium vulnerability (CVSS 6.1) in Harttle Liquidjs, published May 27, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.25%, the 16th percentile. Fixed in liquidjs 10.26.0.
References
github.com/harttle/liquidjs/security/advisories/GHSA-2qv6-9wx5-cwv4
github.com/harttle/liquidjs/commit/26ea2856c7a90aec892b98d94a9b7a3e18539045
github.com/harttle/liquidjs/releases/tag/v10.26.0
nvd.nist.gov/vuln/detail/CVE-2026-44644
github.com/harttle/liquidjs/blob/v10.26.0/CHANGELOG.md
github.com/advisories/GHSA-2qv6-9wx5-cwv4
github.com/CVEProject/cvelistV5/tree/main/cves/2026/44xxx/CVE-2026-44644.json
github.com/harttle/liquidjs
Left out here: the rest of 2 source descriptions. The JSON record has them.