CVE-2022-25948 (TypeScript, published 2022-12-22T05:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: 5.3)
Weakness: CWE-200 \u2014 Exposure of Sensitive Information to an Unauthorized Actor
The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
Repository: https://github.com/harttle/liquidjs
Files changed:
docs/source/tutorials/options.md
docs/source/zh-cn/tutorials/options.md
package.json
src/context/context.ts
src/liquid-options.ts
test/e2e/issues.ts
test/unit/context/context.ts
Commit message: feat: `ownPropertyOnly` option to protect prototype, #454


Vulnerable code:
    "check": "npm run build && npm test && npm run lint",
    if (typeof paths === 'string') paths = paths.split('.')
    return paths.reduce((scope, path) => {
      scope = readProperty(scope, path)
        throw new InternalUndefinedVariableError(path)
export function readProperty (obj: Scope, key: string) {
  if (isFunction(obj[key])) return obj[key]()
      return expect(() => ctx.get(['foo', 'bar', 'not', 'defined'])).to.throw(/undefined variable: bar/)
      return expect(() => ctx.get(['foo', 'BAR'])).to.throw(/undefined variable: BAR/)

Fixed code:
**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`.

**ownPropertyOnly** 用来隐藏原型上的变量，如果你需要把未经处理过的对象传递给模板时，可以设置 `ownPropertyOnly` 为 `true`，默认为 `false`。

    "check": "npm run build && npm test && npm run lint && npm run perf:diff",
    if (isString(paths)) paths = paths.split('.')
    return paths.reduce((scope, path, i) => {
      scope = readProperty(scope, path, this.opts.ownPropertyOnly)
        throw new InternalUndefinedVariableError((paths as string[]).slice(0, i + 1).join!('.'))
export function readProperty (obj: Scope, key: string, ownPropertyOnly: boolean) {
  const jsProperty = readJSProperty(obj, key, ownPropertyOnly)
  if (isFunction(jsProperty)) return jsProperty.call(obj)
  return jsProperty
}
export function readJSProperty (obj: Scope, key: string, ownPropertyOnly: boolean) {
  if (ownPropertyOnly && !Object.hasOwnProperty.call(obj, key)) return undefined
  /** 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;
  /**
   * Same as `ownPropertyOnly` on LiquidOptions, but only for current render() call
   */
  ownPropertyOnly?: boolean;
  ownPropertyOnly: boolean;
  ownPropertyOnly: false,
  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-')
  })
      return expect(() => ctx.get(['foo', 'bar', 'not', 'defined'])).to.throw(/undefined variable: foo.bar/)
      return expect(() => ctx.get(['foo', 'BAR'])).to.throw(/undefined variable: foo.BAR/)
  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/)
    })
  })