mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 12:50:38 -07:00
fix(security): reject render/include partial recursion
Detect cyclic partial rendering via a shared partialStack register (same pattern as CVE-2026-41311 block tag fix). Self-referential or circular {% render %} and {% include %} now throw immediately instead of hanging or OOM.
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -117,4 +117,21 @@ describe('.parseAndRender()', function () {
|
||||
await expect(liquid.renderFile('template')).rejects.toThrow(/block tag cannot be nested/)
|
||||
})
|
||||
})
|
||||
describe('render/include: self-referential partial regression', function () {
|
||||
it('should reject self-referential {% render %} via in-memory templates (no hang / OOM)', async function () {
|
||||
const liquid = new Liquid({ templates: { self: '{% render "self" %}' } })
|
||||
await expect(liquid.parseAndRender('{% render "self" %}')).rejects.toThrow(/render tag cannot be nested/)
|
||||
})
|
||||
it('should reject self-referential {% include %} (no hang / OOM)', async function () {
|
||||
let root: string
|
||||
root = mkdtempSync(join(tmpdir(), 'liquid-e2e-include-nested-'))
|
||||
try {
|
||||
writeFileSync(join(root, 'self.html'), 'A{% include "self.html" %}B')
|
||||
const liquid = new Liquid({ root, extname: '.html' })
|
||||
await expect(liquid.renderFile('self')).rejects.toThrow(/include tag cannot be nested/)
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -296,4 +296,29 @@ describe('tags/include', function () {
|
||||
return expect(html).toBe('FOO-')
|
||||
})
|
||||
})
|
||||
|
||||
describe('recursion', function () {
|
||||
it('should reject self-referential {% include %} (no OOM / hang)', function () {
|
||||
mock({
|
||||
'/self.html': 'A{% include "self.html" %}B'
|
||||
})
|
||||
return expect(liquid.renderFile('/self.html')).rejects.toThrow(/include tag cannot be nested/)
|
||||
})
|
||||
it('should reject indirect {% include %} cycle (no OOM / hang)', function () {
|
||||
mock({
|
||||
'/a.html': '{% include "b.html" %}',
|
||||
'/b.html': '{% include "a.html" %}'
|
||||
})
|
||||
return expect(liquid.renderFile('/a.html')).rejects.toThrow(/include tag cannot be nested/)
|
||||
})
|
||||
it('should allow legitimate nested {% include %} chain', async function () {
|
||||
mock({
|
||||
'/a.html': 'A{% include "b.html" %}',
|
||||
'/b.html': 'B{% include "c.html" %}',
|
||||
'/c.html': 'C'
|
||||
})
|
||||
const html = await liquid.renderFile('/a.html')
|
||||
expect(html).toBe('ABC')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -394,4 +394,27 @@ describe('tags/render', function () {
|
||||
expect(html).toBe('Xchild with redY')
|
||||
})
|
||||
})
|
||||
|
||||
describe('recursion', function () {
|
||||
it('should reject self-referential {% render %} via in-memory templates (no OOM / hang)', async function () {
|
||||
const liquid = new Liquid({ templates: { self: '{% render "self" %}' } })
|
||||
await expect(liquid.parseAndRender('{% render "self" %}')).rejects.toThrow(/render tag cannot be nested/)
|
||||
})
|
||||
it('should reject indirect {% render %} cycle (no OOM / hang)', async function () {
|
||||
mock({
|
||||
'/a.html': '{% render "b.html" %}',
|
||||
'/b.html': '{% render "a.html" %}'
|
||||
})
|
||||
await expect(liquid.renderFile('/a.html')).rejects.toThrow(/render tag cannot be nested/)
|
||||
})
|
||||
it('should allow legitimate nested {% render %} chain', async function () {
|
||||
mock({
|
||||
'/a.html': 'A{% render "b.html" %}',
|
||||
'/b.html': 'B{% render "c.html" %}',
|
||||
'/c.html': 'C'
|
||||
})
|
||||
const html = await liquid.renderFile('/a.html')
|
||||
expect(html).toBe('ABC')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user