diff --git a/src/filters/html.ts b/src/filters/html.ts index 0dd5f77d3..bb603b1dc 100644 --- a/src/filters/html.ts +++ b/src/filters/html.ts @@ -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(/||<[\s\S]*?>|/g, '') + // Single-pass linear strip. The previous regex + // /||<[\s\S]*?>|/g + // backtracks O(n^2) on inputs with many unclosed openers (e.g. `' i) out += str.slice(i, lt) + let end = -1 + if (str.startsWith('', lt + 7) + if (scriptEnd >= 0) end = scriptEnd + 9 + } else if (str.startsWith('', 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 } diff --git a/test/integration/liquid/dos.spec.ts b/test/integration/liquid/dos.spec.ts index 796f2db8e..4b6cfd0df 100644 --- a/test/integration/liquid/dos.spec.ts +++ b/test/integration/liquid/dos.spec.ts @@ -79,5 +79,38 @@ describe('DoS related', function () { await expect(liquid.parseAndRender(src, { array, count: 3 })).resolves.toBe('a a a a a a a a') await expect(liquid.parseAndRender(src, { array, count: 100 })).rejects.toThrow('memory alloc limit exceeded, line:1, col:26') }) + it('should charge strip_html input length to memoryLimit', () => { + const liquid = new Liquid({ memoryLimit: 100 }) + expect(() => liquid.parseAndRenderSync('{{ s | strip_html }}', { s: 'a'.repeat(200) })) + .toThrow('memory alloc limit exceeded') + }) + }) + describe('strip_html ReDoS', () => { + // Regression for O(n^2) backtracking on unclosed ` { + const liquid = new Liquid() + const payload = ' { + const liquid = new Liquid() + const payload = ' but no in linear time', () => { + const liquid = new Liquid() + const payload = '