CVE-2026-44646: Liquidjs True` Vulnerability

MODERATE
Advisory Identifier: GHSA-9x9p-qf8f-mvjg
CVSS 5.3
CWE-693
Published
Advisory Summary

CVE-2026-44646 is a MODERATE (CVSS 5.3) true` vulnerability (CWE-693: Protection Mechanism Failure) affecting Liquidjs libraries in the npm ecosystem. The issue involves true` via `context.spawn()`. Remediation is available in the latest vendor maintenance releases.

CVE-2026-44646 at a Glance

CVE Identifier CVE-2026-44646
GHSA Identifier GHSA-9x9p-qf8f-mvjg
Severity MODERATE
CVSS v3.1 Base Score 5.3
CWE Classification CWE-693: Protection Mechanism Failure
Affected Product Liquidjs
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 5.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-44646

To remediate this vulnerability, upgrade all affected Liquidjs dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

liquidjs fixed: Latest secure release
npm install liquidjs@Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls liquidjs

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-44646, inspect your project dependencies using npm:

npm ls liquidjs

Analyze the terminal output to locate any version of Liquidjs in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: The npm ls liquidjs command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-693: Protection Mechanism Failure, where the product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product. In Liquidjs, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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 // <-- ownPropertyOnly is missing here }, { renderLimit: this.renderLimit, memoryLimit: this.memoryLimit }) } ``` The constructor resolves `ownPropertyOnly` as: ```ts // 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: ```ts * 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: ```ts 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 ```bash 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: 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.

CVE-2026-44646 Affected Liquidjs Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
liquidjs
npm
≤ 10.25.7

References

Frequently Asked Questions

What is CVE-2026-44646?
CVE-2026-44646 is a security vulnerability in Liquidjs involving true` via `Context.spawn()` under CWE-693: Protection Mechanism Failure.
Which Liquidjs versions are affected?
Affected versions include releases prior to current release patches across affected packages: liquidjs.
What version fixes CVE-2026-44646?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is liquidjs affected?
Yes, liquidjs is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of CVE-2026-44646?
CVE-2026-44646 has a CVSS v3.1 base score of 5.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N.
What CWE is associated with CVE-2026-44646?
CVE-2026-44646 is classified under CWE-693: Protection Mechanism Failure (The product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product.).
How can I check npm dependencies for CVE-2026-44646?
Run the dependency tree command: 'npm ls liquidjs'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-9x9p-qf8f-mvjg",
  "modified" : "2026-07-09T21:06:26Z",
  "published" : "2026-05-27T00:28:06Z",
  "aliases" : [ "CVE-2026-44646" ],
  "summary" : "LiquidJS's `{% render %}` tag silently bypasses per-render `ownPropertyOnly:true` via `Context.spawn()`",
  "details" : "## Summary\n\n`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.\n\n## Details\n\nThe bug is in `Context.spawn()`:\n\n```ts\n// src/context/context.ts:105-114\npublic spawn (scope = {}) {\n  return new Context(scope, this.opts, {\n    sync: this.sync,\n    globals: this.globals,\n    strictVariables: this.strictVariables\n    // <-- ownPropertyOnly is missing here\n  }, {\n    renderLimit: this.renderLimit,\n    memoryLimit: this.memoryLimit\n  })\n}\n```\n\nThe constructor resolves `ownPropertyOnly` as:\n\n```ts\n// src/context/context.ts:47\nthis.ownPropertyOnly = renderOptions.ownPropertyOnly ?? opts.ownPropertyOnly\n```\n\nBecause `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.\n\nThe `{% render %}` tag at `src/tags/render.ts:51-77` calls `spawn()` to build the partial's isolated scope:\n\n```ts\n* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {\n  const { liquid, hash } = this\n  const filepath = (yield renderFilePath(this['file'], ctx, liquid)) as string\n  assert(filepath, () => `illegal file path \"${filepath}\"`)\n\n  const childCtx = ctx.spawn()                    // <-- ownPropertyOnly lost here\n  const scope = childCtx.bottom()\n  __assign(scope, yield hash.render(ctx))\n  ...\n  const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile'])) as Template[]\n  yield liquid.renderer.renderTemplates(templates, childCtx, emitter)\n}\n```\n\nAll 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:\n\n```ts\nexport function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) {\n  if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop)) return undefined\n  return obj[key]\n}\n```\n\nThe `{% 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.\n\nTrust 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.\n\n## PoC\n\n```bash\nmkdir -p /tmp/render-poc\nprintf '{{ user.passwordHash }}' > /tmp/render-poc/_user.liquid\n\nnode -e \"\nconst { Liquid } = require('./dist/liquid.node.js');\nconst liquid = new Liquid({ ownPropertyOnly: false, root: '/tmp/render-poc' });\n\nclass User { constructor(n){ this.name = n; } }\nUser.prototype.passwordHash = 'bcrypt\\$secret';\nconst u = new User('alice');\n\nliquid.parseAndRender(\n  'Direct:[{{ user.passwordHash }}] Render:[{% render \\\"_user.liquid\\\", user: user %}]',\n  { user: u },\n  { ownPropertyOnly: true }\n).then(console.log);\n\"\n```\n\nVerified output on liquidjs 10.25.7:\n\n```\nDirect:[] Render:[bcrypt$secret]\n```\n\nThe 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.\n\n## Impact\n\n- **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.\n- **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.\n- **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.\n- **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.\n\n## Recommended Fix\n\nPropagate `ownPropertyOnly` (and any other security-relevant render options) inside `Context.spawn()`:\n\n```ts\n// src/context/context.ts\npublic spawn (scope = {}) {\n  return new Context(scope, this.opts, {\n    sync: this.sync,\n    globals: this.globals,\n    strictVariables: this.strictVariables,\n    ownPropertyOnly: this.ownPropertyOnly   // <-- propagate resolved per-render value\n  }, {\n    renderLimit: this.renderLimit,\n    memoryLimit: this.memoryLimit\n  })\n}\n```\n\nPassing `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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "liquidjs"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "10.25.7"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/harttle/liquidjs/security/advisories/GHSA-9x9p-qf8f-mvjg"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-44646"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/harttle/liquidjs/commit/dbbf6288030591bf6da28d8c1cce5a17bca97bb6"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/harttle/liquidjs"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/harttle/liquidjs/releases/tag/v10.26.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-693" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-27T00:28:06Z",
    "nvd_published_at" : "2026-06-17T23:17:03Z"
  }
}