revert(memory): drop emitter output charge, restore filter output-size accounting

join/array_to_sentence_string/json/inspect charge memoryLimit by the
string they materialize (not element count), so discarded results like
{% assign out = a | join %}{{ out | size }} are still bounded.

Remove the emitter-level limiter added in 2f343f063; it cannot catch
materialized-but-not-emitted values.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Yang Jun
2026-07-06 23:36:22 +08:00
co-authored by Cursor
parent 412efe6a39
commit d6c952a6fc
10 changed files with 50 additions and 37 deletions
+25 -1
View File
@@ -70,7 +70,7 @@ describe('DoS related', function () {
const array = Array(1e3).fill(0)
const liquid = new Liquid({ memoryLimit: 100 })
await expect(liquid.parseAndRender('{{ array | slice: 0, 300 | join }}', { array })).rejects.toThrow('memory alloc limit exceeded, line:1, col:1')
await expect(liquid.parseAndRender('{{ array | slice: 0, 300 | join }}', { array }, { memoryLimit: 2e3 })).resolves.toBe(Array(300).fill(0).join(' '))
await expect(liquid.parseAndRender('{{ array | slice: 0, 300 | join }}', { array }, { memoryLimit: 1e3 })).resolves.toBe(Array(300).fill(0).join(' '))
})
it('should throw for too many array iteration in tags', async () => {
const array = ['a']
@@ -100,12 +100,36 @@ describe('DoS related', function () {
const liquid = new Liquid({ memoryLimit: 100 })
expect(liquid.parseAndRenderSync('{{ array | join: "" }}', { array })).toBe('a'.repeat(20) + 'b'.repeat(20))
})
it('should prevent concat doubling from bypassing join memoryLimit', () => {
const liquid = new Liquid({ memoryLimit: 1e4 })
const src = '{%- assign a = s | split: "NOSEP" -%}' +
'{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}' +
'{{ a | join: "" | size }}'
expect(() => liquid.parseAndRenderSync(src, { s: 'a'.repeat(5000) }))
.toThrow('memory alloc limit exceeded')
})
it('should charge array_to_sentence_string by produced output size', () => {
const array = ['a'.repeat(100), 'b'.repeat(100), 'c'.repeat(100)]
const liquid = new Liquid({ memoryLimit: 100 })
expect(() => liquid.parseAndRenderSync('{{ array | array_to_sentence_string }}', { array }))
.toThrow('memory alloc limit exceeded')
})
it('should charge json serialization of concat-doubled arrays', () => {
const liquid = new Liquid({ memoryLimit: 1e4 })
const src = '{%- assign a = s | split: "NOSEP" -%}' +
'{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}' +
'{{ a | json | size }}'
expect(() => liquid.parseAndRenderSync(src, { s: 'a'.repeat(5000) }))
.toThrow('memory alloc limit exceeded')
})
it('should charge inspect serialization of concat-doubled arrays', () => {
const liquid = new Liquid({ memoryLimit: 1e4 })
const src = '{%- assign a = s | split: "NOSEP" -%}' +
'{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}{%- assign a = a | concat: a -%}' +
'{{ a | inspect | size }}'
expect(() => liquid.parseAndRenderSync(src, { s: 'a'.repeat(5000) }))
.toThrow('memory alloc limit exceeded')
})
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) }))