Information Exposure
Description
The package liquidjs before 10.0.0 are vulnerable to Information Exposure when ownPropertyOnly parameter is set to False, which results in leaking properties of a prototype. Workaround For versions 9.34.0 and higher, an option to disable this functionality is provided.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
LiquidJS before 10.0.0 exposes prototype properties when ownPropertyOnly is false, leading to information disclosure.
Vulnerability
Overview
The vulnerability in LiquidJS (CVE-2022-25948) is an information exposure flaw that arises when the ownPropertyOnly parameter is set to False (the default in versions prior to 10.0.0). In this mode, the template engine does not restrict property access to an object's own properties, allowing template expressions to traverse the prototype chain and access inherited properties [1][4]. This includes properties like constructor, __proto__, and built-in methods, which can leak internal object details.
Exploitation
An attacker can exploit this by providing a malicious Liquid template to an application that uses the vulnerable library. No authentication is required if the application renders user-supplied templates. By accessing prototype properties, the attacker can enumerate object internals, potentially extracting sensitive configuration data or application state [2][4]. The attack surface is broad because LiquidJS is commonly used in server-side rendering contexts.
Impact
Successful exploitation allows an attacker to read prototype properties of objects accessible within the template context. This information exposure can reveal internal application logic, environment variables, or other secrets, which may be leveraged for further attacks such as privilege escalation or remote code execution [1][4]. The severity is amplified when templates are rendered with access to sensitive objects.
Mitigation
The vulnerability is fixed in LiquidJS version 10.0.0, where the ownPropertyOnly option defaults to true, preventing prototype property access [3]. For users on versions 9.34.0 and higher, a workaround is available by explicitly setting ownPropertyOnly: true in the template engine options [2][4]. Upgrading to the latest version is strongly recommended to fully remediate the issue.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
liquidjsnpm | < 10.0.0 | 10.0.0 |
Affected products
2- liquidjs/liquidjsdescription
Patches
27eb621601c2brefactor: change `ownPropertyOnly` default value to `true`
3 files changed · +10 −10
package-lock.json+3 −3 modified@@ -2586,9 +2586,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001335", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001335.tgz", - "integrity": "sha512-ddP1Tgm7z2iIxu6QTtbZUv6HJxSaV/PZeSrWFZtbY4JZ69tOeNhBCl3HyRQgeNZKE5AOn1kpV7fhljigy0Ty3w==", + "version": "1.0.30001431", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", + "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==", "dev": true }, "cardinal": {
src/context/context.ts+6 −6 modified@@ -103,19 +103,19 @@ export class Context { } export function readProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) { - if (isNil(obj)) return obj obj = toLiquid(obj) + if (isNil(obj)) return obj if (isArray(obj) && key < 0) return obj[obj.length + +key] - const jsProperty = readJSProperty(obj, key, ownPropertyOnly) - if (jsProperty === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key) - if (isFunction(jsProperty)) return jsProperty.call(obj) + const value = readJSProperty(obj, key, ownPropertyOnly) + if (value === undefined && obj instanceof Drop) return obj.liquidMethodMissing(key) + if (isFunction(value)) return value.call(obj) if (key === 'size') return readSize(obj) else if (key === 'first') return readFirst(obj) else if (key === 'last') return readLast(obj) - return jsProperty + return value } export function readJSProperty (obj: Scope, key: PropertyKey, ownPropertyOnly: boolean) { - if (ownPropertyOnly && !Object.hasOwnProperty.call(obj, key)) return undefined + if (ownPropertyOnly && !Object.hasOwnProperty.call(obj, key) && !(obj instanceof Drop)) return undefined return obj[key] }
src/liquid-options.ts+1 −1 modified@@ -153,7 +153,7 @@ export const defaultOptions: NormalizedFullOptions = { preserveTimezones: false, strictFilters: false, strictVariables: false, - ownPropertyOnly: false, + ownPropertyOnly: true, lenientIf: false, globals: {}, keepOutputType: false,
7e99efc5131efeat: `ownPropertyOnly` option to protect prototype, #454
7 files changed · +93 −9
docs/source/tutorials/options.md+2 −0 modified@@ -128,6 +128,8 @@ it defaults to false. For example, when set to true, a blank string would evalu **lenientIf** modifies the behavior of `strictVariables` to allow handling optional variables. If set to `true`, an undefined variable will *not* cause an exception in the following two situations: a) it is the condition to an `if`, `elsif`, or `unless` tag; b) it occurs right before a `default` filter. Irrelevant if `strictVariables` is not set. Defaults to `false`. +**ownPropertyOnly** hides scope variables from prototypes, useful when you're passing a not sanitized object into LiquidJS or need to hide prototypes from templates. Defaults to `false`. + {% note info Non-existent Tags %} Non-existent tags always throw errors during parsing and this behavior can not be customized. {% endnote %}
docs/source/zh-cn/tutorials/options.md+2 −0 modified@@ -125,6 +125,8 @@ LiquidJS 把这个选项默认值设为 <code>true</code> 以兼容于 shopify/l **strictVariables** 用来启用变量严格模式。如果设置为 `true` 变量不存在时渲染会抛出异常,默认为 `false` 这时不存在的变量会被渲染为空字符串。 +**ownPropertyOnly** 用来隐藏原型上的变量,如果你需要把未经处理过的对象传递给模板时,可以设置 `ownPropertyOnly` 为 `true`,默认为 `false`。 + {% note info 不存在的标签 %} 不存在的标签总是会抛出一个解析异常,这一行为无法自定义。 {% endnote %}
package.json+1 −1 modified@@ -12,7 +12,7 @@ "types": "dist/liquid.d.ts", "scripts": { "lint": "eslint \"**/*.ts\" .", - "check": "npm run build && npm test && npm run lint", + "check": "npm run build && npm test && npm run lint && npm run perf:diff", "test": "nyc mocha \"test/**/*.ts\"", "test:e2e": "mocha \"test/e2e/**/*.ts\"", "perf": "cd benchmark && npm ci && npm start",
src/context/context.ts+11 −6 modified@@ -59,11 +59,11 @@ export class Context { return this.getFromScope(scope, paths) } public getFromScope (scope: object, paths: string[] | string) { - if (typeof paths === 'string') paths = paths.split('.') - return paths.reduce((scope, path) => { - scope = readProperty(scope, path) + if (isString(paths)) paths = paths.split('.') + return paths.reduce((scope, path, i) => { + scope = readProperty(scope, path, this.opts.ownPropertyOnly) if (isNil(scope) && this.strictVariables) { - throw new InternalUndefinedVariableError(path) + throw new InternalUndefinedVariableError((paths as string[]).slice(0, i + 1).join!('.')) } return scope }, scope) @@ -87,17 +87,22 @@ export class Context { } } -export function readProperty (obj: Scope, key: string) { +export function readProperty (obj: Scope, key: string, ownPropertyOnly: boolean) { if (isNil(obj)) return obj obj = toLiquid(obj) - if (isFunction(obj[key])) return obj[key]() + const jsProperty = readJSProperty(obj, key, ownPropertyOnly) + if (isFunction(jsProperty)) return jsProperty.call(obj) if (obj instanceof Drop) { if (obj.hasOwnProperty(key)) return obj[key] return obj.liquidMethodMissing(key) } if (key === 'size') return readSize(obj) if (key === 'first') return readFirst(obj) if (key === 'last') return readLast(obj) + return jsProperty +} +export function readJSProperty (obj: Scope, key: string, ownPropertyOnly: boolean) { + if (ownPropertyOnly && !Object.hasOwnProperty.call(obj, key)) return undefined return obj[key] }
src/liquid-options.ts+8 −0 modified@@ -31,6 +31,8 @@ export interface LiquidOptions { strictFilters?: boolean; /** Whether or not to assert variable existence. If set to `false`, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause an exception. Defaults to `false`. */ strictVariables?: boolean; + /** Hide scope variables from prototypes, useful when you're passing a not sanitized object into LiquidJS or need to hide prototypes from templates. */ + ownPropertyOnly?: boolean; /** Modifies the behavior of `strictVariables`. If set, a single undefined variable will *not* cause an exception in the context of the `if`/`elsif`/`unless` tag and the `default` filter. Instead, it will evaluate to `false` and `null`, respectively. Irrelevant if `strictVariables` is not set. Defaults to `false`. **/ lenientIf?: boolean; /** JavaScript timezoneOffset for `date` filter, default to local time. That means if you're in Australia (UTC+10), it'll default to -600 */ @@ -80,6 +82,10 @@ export interface RenderOptions { * Same as `strictVariables` on LiquidOptions, but only for current render() call */ strictVariables?: boolean; + /** + * Same as `ownPropertyOnly` on LiquidOptions, but only for current render() call + */ + ownPropertyOnly?: boolean; } interface NormalizedOptions extends LiquidOptions { @@ -103,6 +109,7 @@ export interface NormalizedFullOptions extends NormalizedOptions { fs: FS; strictFilters: boolean; strictVariables: boolean; + ownPropertyOnly: boolean; lenientIf: boolean; trimTagRight: boolean; trimTagLeft: boolean; @@ -143,6 +150,7 @@ export const defaultOptions: NormalizedFullOptions = { preserveTimezones: false, strictFilters: false, strictVariables: false, + ownPropertyOnly: false, lenientIf: false, globals: {}, keepOutputType: false,
test/e2e/issues.ts+5 −0 modified@@ -194,4 +194,9 @@ describe('Issues', function () { const html = engine.parseAndRenderSync(template, { array: [1, 2, 3] }) expect(html).to.equal('4#8#12#6') }) + it('#454 leaking JS prototype getter functions in evaluation', async () => { + const engine = new Liquid({ ownPropertyOnly: true }) + const html = engine.parseAndRenderSync('{{foo | size}}-{{bar.coo}}', { foo: 'foo', bar: Object.create({ coo: 'COO' }) }) + expect(html).to.equal('3-') + }) })
test/unit/context/context.ts+64 −2 modified@@ -95,11 +95,11 @@ describe('Context', function () { }) it('should throw when deep variable not exist', async function () { ctx.push({ foo: 'FOO' }) - return expect(() => ctx.get(['foo', 'bar', 'not', 'defined'])).to.throw(/undefined variable: bar/) + return expect(() => ctx.get(['foo', 'bar', 'not', 'defined'])).to.throw(/undefined variable: foo.bar/) }) it('should throw when itself not defined', async function () { ctx.push({ foo: 'FOO' }) - return expect(() => ctx.get(['foo', 'BAR'])).to.throw(/undefined variable: BAR/) + return expect(() => ctx.get(['foo', 'BAR'])).to.throw(/undefined variable: foo.BAR/) }) it('should find variable in parent scope', async function () { ctx.push({ 'foo': 'foo' }) @@ -110,6 +110,68 @@ describe('Context', function () { }) }) + describe('ownPropertyOnly', async function () { + let ctx: Context + beforeEach(function () { + ctx = new Context(ctx, { + ownPropertyOnly: true + } as any) + }) + it('should return undefined for prototype object property', function () { + ctx.push({ foo: Object.create({ bar: 'BAR' }) }) + return expect(ctx.get(['foo', 'bar'])).to.equal(undefined) + }) + it('should return undefined for Array.prototype.reduce', function () { + ctx.push({ foo: [] }) + return expect(ctx.get(['foo', 'reduce'])).to.equal(undefined) + }) + it('should return undefined for function prototype property', function () { + function Foo () {} + Foo.prototype.bar = 'BAR' + ctx.push({ foo: new (Foo as any)() }) + return expect(ctx.get(['foo', 'bar'])).to.equal(undefined) + }) + it('should allow function constructor properties', function () { + function Foo (this: any) { this.bar = 'BAR' } + ctx.push({ foo: new (Foo as any)() }) + return expect(ctx.get(['foo', 'bar'])).to.equal('BAR') + }) + it('should return undefined for class method', function () { + class Foo { bar () {} } + ctx.push({ foo: new Foo() }) + return expect(ctx.get(['foo', 'bar'])).to.equal(undefined) + }) + it('should allow class property', function () { + class Foo { bar = 'BAR' } + ctx.push({ foo: new Foo() }) + return expect(ctx.get(['foo', 'bar'])).to.equal('BAR') + }) + it('should allow Array.prototype.length', function () { + ctx.push({ foo: [1, 2] }) + return expect(ctx.get(['foo', 'length'])).to.equal(2) + }) + it('should allow size to access Array.prototype.length', function () { + ctx.push({ foo: [1, 2] }) + return expect(ctx.get(['foo', 'size'])).to.equal(2) + }) + it('should allow size to access Set.prototype.size', function () { + ctx.push({ foo: new Set([1, 2]) }) + return expect(ctx.get(['foo', 'size'])).to.equal(2) + }) + it('should allow size to access Object key count', function () { + ctx.push({ foo: { bar: 'BAR', coo: 'COO' } }) + return expect(ctx.get(['foo', 'size'])).to.equal(2) + }) + it('should throw when property is hidden and strictVariables is true', function () { + ctx = new Context(ctx, { + ownPropertyOnly: true, + strictVariables: true + } as any) + ctx.push({ foo: Object.create({ bar: 'BAR' }) }) + return expect(() => ctx.get(['foo', 'bar'])).to.throw(/undefined variable: foo.bar/) + }) + }) + describe('.getAll()', function () { it('should get all properties when arguments empty', async function () { expect(ctx.getAll()).deep.equal(scope)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-45rm-2893-5f49ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-25948ghsaADVISORY
- github.com/harttle/liquidjs/commit/7e99efc5131e20cf3f59e1fc2c371a15aa4109dbghsaWEB
- github.com/harttle/liquidjs/commit/7eb621601c2b05d6e379e5ce42219f2b1f556208ghsaWEB
- github.com/harttle/liquidjs/issues/454ghsaWEB
- groups.google.com/u/0/a/snyk.io/g/report/c/9ipXecWRtTM/m/IgLadevtCQAJghsaWEB
- security.snyk.io/vuln/SNYK-JS-LIQUIDJS-2952868ghsaWEB
News mentions
0No linked articles in our index yet.