From dbbf6288030591bf6da28d8c1cce5a17bca97bb6 Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Sun, 3 May 2026 22:35:31 +0800 Subject: [PATCH] fix: propagate ownPropertyOnly into Context.spawn() for {% render %} (#893) 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 --- src/context/context.ts | 3 ++- test/integration/tags/render.spec.ts | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/context/context.ts b/src/context/context.ts index 5a0fe576a..205a57622 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -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 diff --git a/test/integration/tags/render.spec.ts b/test/integration/tags/render.spec.ts index 1ce90c4ab..2530cec6a 100644 --- a/test/integration/tags/render.spec.ts +++ b/test/integration/tags/render.spec.ts @@ -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(() => {