refactor(strip_html): drop position cache, delete dead blocks from Set

Once `indexOf(closer, X)` returns -1, all subsequent searches (with
monotonically increasing start) also return -1. So tracking absence is
enough; storing positions is unnecessary. Make `blocks` a Set and
delete a kind once its closer is known absent — no parallel `dead`
bookkeeping. Use Jest's per-test timeout for the ReDoS regressions
instead of manual Date.now() bookkeeping.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Yang Jun
2026-05-11 23:25:31 +08:00
co-authored by Cursor
parent 0681ff843c
commit 7a77fa4f64
2 changed files with 14 additions and 27 deletions
+7 -15
View File
@@ -88,29 +88,21 @@ describe('DoS related', function () {
describe('strip_html ReDoS', () => {
// Regression for O(n^2) backtracking on unclosed `<script` / `<style` openers.
// The previous regex stalled the event loop for ~10s on 350KB of `'<script'.repeat`.
// The per-test timeout below caps total time; an O(n^2) regression would blow it.
it('should handle many unclosed <script openers in linear time', () => {
const liquid = new Liquid()
const payload = '<script'.repeat(50000)
const t0 = Date.now()
const out = liquid.parseAndRenderSync('{{ x | strip_html }}', { x: payload })
expect(Date.now() - t0).toBeLessThan(1000)
expect(out).toBe(payload)
})
expect(liquid.parseAndRenderSync('{{ x | strip_html }}', { x: payload })).toBe(payload)
}, 1000)
it('should handle many unclosed <style openers in linear time', () => {
const liquid = new Liquid()
const payload = '<style'.repeat(50000)
const t0 = Date.now()
const out = liquid.parseAndRenderSync('{{ x | strip_html }}', { x: payload })
expect(Date.now() - t0).toBeLessThan(1000)
expect(out).toBe(payload)
})
expect(liquid.parseAndRenderSync('{{ x | strip_html }}', { x: payload })).toBe(payload)
}, 1000)
it('should handle <script openers that have > but no </script> in linear time', () => {
const liquid = new Liquid()
const payload = '<script>foo'.repeat(50000)
const t0 = Date.now()
const out = liquid.parseAndRenderSync('{{ x | strip_html }}', { x: payload })
expect(Date.now() - t0).toBeLessThan(1000)
expect(out).toBe('foo'.repeat(50000))
})
expect(liquid.parseAndRenderSync('{{ x | strip_html }}', { x: payload })).toBe('foo'.repeat(50000))
}, 1000)
})
})