GitHub Advisoryadvisory · npm

GHSA-9x9p-qf8f-mvjg LiquidJS's `{% render %}` tag silently bypasses per-render `ownPropertyOnly:true` via `Context.spawn()`

MEDIUM Published 2026-05-27 Modified 2026-10-27 Official page ↗
Aliases
CVE-2026-44646
Ecosystem
npm
Packages
liquidjs
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Weaknesses
CWE-693
Reviewed
2026-05-27

Affected packages

• npm liquidjs: < 10.26.0 → fixed in 10.26.0

Description

Summary `Context.spawn()` in liquidjs creates a child `Context` for the `{% render %}` tag but does not propagate the parent context's resolved `ownPropertyOnly` value. The new context re-derives `ownPropertyOnly` from `opts.ownPropertyOnly` (the instance-level option), silently discarding any `RenderOptions.ownPropertyOnly` override that was supplied to `parseAndRender()`. As a result, a developer who runs a Liquid instance with the backwards-compatible `ownPropertyOnly:false` and then locks down an untrusted render with `parseAndRender(..., { ownPropertyOnly: true })` still leaks prototype-chain properties from inside any `{% render %}` partial. This is a distinct exploit surface from the previously identified array-filter variants (`where`, `reject`, `group_by`, `find`, `find_index`, `has`) — the underlying root cause in `Context.spawn()` is shared, but `{% render %}` is a separately reachable sink that needs no filter usage. Details The bug is in `Context.spawn()`: ```ts // src/context/context.ts:105-114 public spawn (scope = {}) { return new Context(scope, this.opts, { sync: this.sync, globals: this.globals, strictVariables: this.strictVariables // { const { liquid, hash } = this const filepath = (yield renderFilePath(this['file'], ctx, liquid)) as string assert(filepath, () => `illegal file path "${filepath}"`) const childCtx = ctx.spawn() // /tmp/render-poc/_user.liquid node -e " const { Liquid } = require('./dist/liquid.node.js'); const liquid = new Liquid({ ownPropertyOnly: false, root: '/tmp/render-poc' }); class User { constructor(n){ this.name = n; } } User.prototype.passwordHash = 'bcrypt\$secret'; const u = new User('alice'); liquid.parseAndRender( 'Direct:[{{ user.passwordHash }}] Render:[{% render \"_user.liquid\", user: user %}]', { user: u }, { ownPropertyOnly: true } ).then(console.log); " ``` Verified output on liquidjs 10.25.7: ``` Direct:[] Render:[bcrypt$secret] ``` The top-level expression `{{ user.passwordHash }}` is correctly blocked by the per-render `ownPropertyOnly:true`, but the same expression inside the partial loaded by `{% render %}` returns the prototype-chain property — proof that `Context.spawn()` discarded the override. Impact • Information disclosure: Any prototype-chain property of objects passed into a `{% render %}` partial — including secrets, hashes, internal state, framework-injected helpers — becomes readable from inside the partial template, even when the developer used the documented per-render lockdown. • Realistic threat model: Applications that maintain `ownPropertyOnly:false` for backwards compatibility (or because their data layer relies on prototype methods) and lock down untrusted-template renders with `parseAndRender(..., { ownPropertyOnly:true })` are protected at the top level but silently exposed inside any partial. User-controllable template content (CMS snippets, theme partials, email templates) that uses `{% render %}` becomes an info-leak primitive. • Distinct from existing CVE-2022-25948: the prior advisory only covered direct use of `ownPropertyOnly:false`; this is a failure of the documented mitigation (`ownPropertyOnly:true` per-render override), not a missing setting. • Distinct from the array-filter variant: same `spawn()` root cause, but exploitable without invoking `where/reject/group_by/find/find_index/has` — only requires that the template uses `{% render %}` (a basic templating feature) and that one of the rendered values has prototype-chain properties. Recommended Fix Propagate `ownPropertyOnly` (and any other security-relevant render options) inside `Context.spawn()`: ```ts // src/context/context.ts public spawn (scope = {}) { return new Context(scope, this.opts, { sync: this.sync, globals: this.globals, strictVariables: this.strictVariables, ownPropertyOnly: this.ownPropertyOnly // <-- propagate resolved per-render value }, { renderLimit: this.renderLimit, memoryLimit: this.memoryLimit }) } ``` Passing `this.ownPropertyOnly` (the resolved value, not `this.opts.ownPropertyOnly`) ensures any `RenderOptions.ownPropertyOnly` override flows into spawned child contexts. This single change closes both the `{% render %}` pathway documented here and the array-filter pathway tracked separately. A regression test should assert that a partial rendered via `{% render %}` honours `parseAndRender(..., { ownPropertyOnly: true })` against an object with prototype-chain properties.

Related

Alias · CVE (1)

  • CVE-2026-44646 LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. vulnerability

Weakness · CWE (1)

  • CWE-693 Protection Mechanism Failure weakness

References