VYPR
Moderate severityNVD Advisory· Published Dec 23, 2022· Updated Apr 14, 2025

Information Exposure

CVE-2022-25948

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.

PackageAffected versionsPatched versions
liquidjsnpm
< 10.0.010.0.0

Affected products

2

Patches

2
7eb621601c2b

refactor: change `ownPropertyOnly` default value to `true`

https://github.com/harttle/liquidjsJun YangNov 17, 2022via ghsa
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,
    
7e99efc5131e

feat: `ownPropertyOnly` option to protect prototype, #454

https://github.com/harttle/liquidjsHarttleJan 28, 2022via ghsa
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

News mentions

0

No linked articles in our index yet.