Every CVE Search/ Get API key

CVE-2026-44646

LiquidJS: `{% render %}` tag silently bypasses per-render `ownPropertyOnly:true` via `Context.spawn()`

5.3 MediumFix available

LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, Context.spawn() creates a child Context for the {% render %} tag but does not propagate the parent context's resolved ownPropertyOnly value, resulting in a silent bypass. 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. This issue has been fixed in version 10.26.0.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
Low
Integrity
None
Availability
None

Exploit likelihood

0.33%Up 0.3 points since Jun 12, 2026
90%50%10%1%

What each source reports

CVE List Score used 5.3

LiquidJS: `{% render %}` tag silently bypasses per-render `ownPropertyOnly:true` via `Context.spawn()`

LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, Context.spawn() creates a child Context for the {% render %} tag but does not propagate the parent context's resolved ownPropertyOnly value, resulting in a silent bypass. 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. This issue has been fixed in version 10.26.0.

Record
CVE-2026-44646
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Weakness
CWE-693
Updated
2026-11-02
EUVDEUVD-2026-37826 5.3

LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, Context.spawn() creates a child Context for the {% render %} tag but does not propagate the parent context's resolved ownPropertyOnly value, resulting in a silent bypass. 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. This issue has been fixed in version 10.26.0.

Record
EUVD-2026-37826
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-9x9p-qf8f-mvjg 5.3

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

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():

// src/context/context.ts:105-114
public spawn (scope = {}) {
  return new Context(scope, this.opts, {
    sync: this.sync,
    globals: this.globals,
    strictVariables: this.strictVariables
    // <-- ownPropertyOnly is missing here
  }, {
    renderLimit: this.renderLimit,
    memoryLimit: this.memoryLimit
  })
}

The constructor resolves ownPropertyOnly as:

// src/context/context.ts:47
this.ownPropertyOnly = renderOptions.ownPropertyOnly ?? opts.ownPropertyOnly

Because spawn() passes a RenderOptions object with no ownPropertyOnly, the child context falls back to opts.ownPropertyOnly (the instance-level option), throwing away any per-render override that the parent context had applied. this.opts is the raw normalized instance options object; it is not mutated to reflect render-time overrides.

The {% render %} tag at src/tags/render.ts:51-77 calls spawn() to build the partial's isolated scope:

* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
  const { liquid, hash } = this
  const filepath = (yield renderFilePath(this['file'], ctx, liquid)) as string
  assert(filepath, () => `illegal file path "${filepath}"`)

  const childCtx = ctx.spawn()                    // <-- ownPropertyOnly lost here
  const scope = childCtx.bottom()
  __assign(scope, yield hash.render(ctx))
  ...
  const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile'])) as Template[]
  yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
}

All template variable lookups inside the partial then go through childCtx.readProperty() (src/context/context.ts:123-135), which calls readJSProperty(obj, key, this.ownPropertyOnly). With childCtx.ownPropertyOnly === false (inherited from opts), the protective check at src/context/context.ts:138-141 is skipped and prototype-chain properties are returned to the template:

export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) {
  if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop)) return undefined
  return obj[key]
}

The {% include %} tag is not affected: it does not call spawn(); it pushes onto the parent context's scope stack (src/tags/include.ts:40), so the parent's resolved ownPropertyOnly continues to apply.

Trust model / why this matters: RenderOptions.ownPropertyOnly is documented (src/liquid-options.ts:108-111) as "Same as ownPropertyOnly on LiquidOptions, but only for current render() call". It exists precisely so that developers running a non-strict instance can lock down individual untrusted renders. That contract is broken — the override is silently dropped at every partial boundary.

PoC

mkdir -p /tmp/render-poc
printf '{{ user.passwordHash }}' > /tmp/render-poc/_user.liquid

node -e "
const { Liquid } = require('./dist/liquid.node.js');
const liquid = new Liquid({ ownPropertyOnly:…
Record
GHSA-9x9p-qf8f-mvjg
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Weakness
CWE-693
Updated
2026-11-02
NVD 5.3

LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, Context.spawn() creates a child Context for the {% render %} tag but does not propagate the parent context's resolved ownPropertyOnly value, resulting in a silent bypass. 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. This issue has been fixed in version 10.26.0.

Record
CVE-2026-44646
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Weakness
CWE-693
Updated
2026-11-02
OSV 5.3

LiquidJS: `{% render %}` tag silently bypasses per-render `ownPropertyOnly:true` via `Context.spawn()`

LiquidJS is a Shopify/GitHub Pages compatible template engine written in pure JavaScript. In versions 10.25.7 and below, Context.spawn() creates a child Context for the {% render %} tag but does not propagate the parent context's resolved ownPropertyOnly value, resulting in a silent bypass. 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. This issue has been fixed in version 10.26.0.

Record
CVE-2026-44646
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Weakness
CWE-693
Updated
2026-11-02
OSVGHSA-9x9p-qf8f-mvjg 5.3

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

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():

// src/context/context.ts:105-114
public spawn (scope = {}) {
  return new Context(scope, this.opts, {
    sync: this.sync,
    globals: this.globals,
    strictVariables: this.strictVariables
    // <-- ownPropertyOnly is missing here
  }, {
    renderLimit: this.renderLimit,
    memoryLimit: this.memoryLimit
  })
}

The constructor resolves ownPropertyOnly as:

// src/context/context.ts:47
this.ownPropertyOnly = renderOptions.ownPropertyOnly ?? opts.ownPropertyOnly

Because spawn() passes a RenderOptions object with no ownPropertyOnly, the child context falls back to opts.ownPropertyOnly (the instance-level option), throwing away any per-render override that the parent context had applied. this.opts is the raw normalized instance options object; it is not mutated to reflect render-time overrides.

The {% render %} tag at src/tags/render.ts:51-77 calls spawn() to build the partial's isolated scope:

* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
  const { liquid, hash } = this
  const filepath = (yield renderFilePath(this['file'], ctx, liquid)) as string
  assert(filepath, () => `illegal file path "${filepath}"`)

  const childCtx = ctx.spawn()                    // <-- ownPropertyOnly lost here
  const scope = childCtx.bottom()
  __assign(scope, yield hash.render(ctx))
  ...
  const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile'])) as Template[]
  yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
}

All template variable lookups inside the partial then go through childCtx.readProperty() (src/context/context.ts:123-135), which calls readJSProperty(obj, key, this.ownPropertyOnly). With childCtx.ownPropertyOnly === false (inherited from opts), the protective check at src/context/context.ts:138-141 is skipped and prototype-chain properties are returned to the template:

export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) {
  if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop)) return undefined
  return obj[key]
}

The {% include %} tag is not affected: it does not call spawn(); it pushes onto the parent context's scope stack (src/tags/include.ts:40), so the parent's resolved ownPropertyOnly continues to apply.

Trust model / why this matters: RenderOptions.ownPropertyOnly is documented (src/liquid-options.ts:108-111) as "Same as ownPropertyOnly on LiquidOptions, but only for current render() call". It exists precisely so that developers running a non-strict instance can lock down individual untrusted renders. That contract is broken — the override is silently dropped at every partial boundary.

PoC

mkdir -p /tmp/render-poc
printf '{{ user.passwordHash }}' > /tmp/render-poc/_user.liquid

node -e "
const { Liquid } = require('./dist/liquid.node.js');
const liquid = new Liquid({ ownPropertyOnly:…
Record
GHSA-9x9p-qf8f-mvjg
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Weakness
CWE-693
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
liquidjs
npm
before 10.26.0
10.26.0

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-44646 is a medium vulnerability (CVSS 5.3) in Harttle Liquidjs, published May 27, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.33%, the 26th percentile. Fixed in liquidjs 10.26.0.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open