mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 12:50:38 -07:00
fix(strip_html): rewrite as linear single-pass scan to avoid ReDoS
The previous strip_html regex /<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<[\s\S]*?>|<!--[\s\S]*?-->/g contains lazy alternatives that backtrack O(n^2) on inputs with many unclosed `<script` / `<style` openers. A 350KB payload of `'<script'.repeat(50000)` blocked the Node.js event loop for ~10s, and cost grew quadratically with input size. memoryLimit only charged str.length, which does not bound regex CPU. Replace the regex with an indexOf-based single-pass scan. For each `<` we: - if `<script` opener: find next `</script>` and skip the whole block; cache "no closer after pos k" so subsequent unclosed `<script` openers do not re-scan the tail. - same for `<style` / `</style>`. - otherwise treat as a generic `<...>` tag (matches the original behavior, where the `<[\s\S]*?>` alternative also caught comments). - if no closing `>` exists, emit the tail as literal text and stop. Total work is O(n). All existing strip_html test cases pass unchanged. Add regression tests covering the PoCs (`<script` / `<style` repeats, and `<script>foo` repeats with `>` but no `</script>`) plus a memoryLimit assertion. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+30
-1
@@ -45,5 +45,34 @@ export function newline_to_br (this: FilterImpl, v: string) {
|
||||
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]*?>|<!--[\s\S]*?-->/g, '')
|
||||
// Single-pass linear strip. The previous regex
|
||||
// /<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>|<[\s\S]*?>|<!--[\s\S]*?-->/g
|
||||
// backtracks O(n^2) on inputs with many unclosed openers (e.g. `'<script'.repeat(N)`).
|
||||
// We use indexOf-based scanning and cache "no closer after position k" results so that
|
||||
// repeated unclosed openers do not re-scan the tail. Total work is O(n).
|
||||
let out = ''
|
||||
let i = 0
|
||||
const n = str.length
|
||||
let scriptEnd = 0
|
||||
let styleEnd = 0
|
||||
while (i < n) {
|
||||
const lt = str.indexOf('<', i)
|
||||
if (lt < 0) { out += str.slice(i); break }
|
||||
if (lt > i) out += str.slice(i, lt)
|
||||
let end = -1
|
||||
if (str.startsWith('<script', lt)) {
|
||||
if (scriptEnd !== -1 && scriptEnd < lt + 7) scriptEnd = str.indexOf('</script>', lt + 7)
|
||||
if (scriptEnd >= 0) end = scriptEnd + 9
|
||||
} else if (str.startsWith('<style', lt)) {
|
||||
if (styleEnd !== -1 && styleEnd < lt + 6) styleEnd = str.indexOf('</style>', lt + 6)
|
||||
if (styleEnd >= 0) end = styleEnd + 8
|
||||
}
|
||||
if (end < 0) {
|
||||
const gt = str.indexOf('>', lt + 1)
|
||||
if (gt < 0) { out += str.slice(lt); break }
|
||||
end = gt + 1
|
||||
}
|
||||
i = end
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user