Compare commits

...
Author SHA1 Message Date
Yang JunandCursor 1318b3f3ed fix: propagate ownPropertyOnly into Context.spawn() for {% render %}
Child contexts from spawn() re-derived ownPropertyOnly from Liquid opts
only, dropping per-render RenderOptions overrides. That broke the contract
that parseAndRender(..., { ownPropertyOnly: true }) locks down a single
render, including partials loaded via {% render %}.

Add regression test matching prototype-chain leak PoC.

Co-authored-by: Cursor <[email protected]>
2026-05-03 21:58:55 +08:00
2 changed files with 23 additions and 1 deletions
+2 -1
View File
@@ -106,7 +106,8 @@ export class Context {
return new Context(scope, this.opts, {
sync: this.sync,
globals: this.globals,
strictVariables: this.strictVariables
strictVariables: this.strictVariables,
ownPropertyOnly: this.ownPropertyOnly
}, {
renderLimit: this.renderLimit,
memoryLimit: this.memoryLimit
+21
View File
@@ -271,6 +271,27 @@ describe('tags/render', function () {
return expect(staticLiquid.renderFile('parent.html')).rejects.toThrow(/Failed to lookup "..\/bar\/child.html"/)
})
describe('per-render ownPropertyOnly', function () {
it('should propagate to {% render %} partial (spawned context)', async function () {
mock({
'/_user.liquid': '{{ user.passwordHash }}'
})
const engine = new Liquid({ ownPropertyOnly: false, root: '/' })
class User {
name: string
constructor (n: string) {
this.name = n
}
}
Object.assign(User.prototype, { passwordHash: 'secret-from-prototype' })
const u = new User('alice')
const tpl = 'Direct:[{{ user.passwordHash }}] Render:[{% render "_user.liquid", user: user %}]'
const html = await engine.parseAndRender(tpl, { user: u }, { ownPropertyOnly: true })
expect(html).toBe('Direct:[] Render:[]')
expect(engine.parseAndRenderSync(tpl, { user: u }, { ownPropertyOnly: true })).toBe('Direct:[] Render:[]')
})
})
describe('static partial', function () {
let staticLiquid: Liquid
beforeEach(() => {