diff --git a/test/e2e/issues.spec.ts b/test/e2e/issues.spec.ts index c5243fe66..fea07b3cd 100644 --- a/test/e2e/issues.spec.ts +++ b/test/e2e/issues.spec.ts @@ -77,6 +77,16 @@ describe('Issues', function () { ) expect(html).toBe('BAR') }) + it('filter/tag lookup must not inherit Object.prototype (node + UMD)', async () => { + const tpl = '{% assign r = 1 | valueOf %}{{ r.context }}{{ r.liquid }}{{ r.token }}|{{ r }}' + const nodeEngine = new Liquid() + expect(await nodeEngine.parseAndRender(tpl)).toBe('|1') + expect(() => nodeEngine.parse('{% constructor %}')).toThrow('tag "constructor" not found') + + const umdEngine = new LiquidUMD() + expect(await umdEngine.parseAndRender(tpl)).toBe('|1') + expect(() => umdEngine.parse('{% constructor %}')).toThrow('tag "constructor" not found') + }) it('lenientIf not working as expected in umd #313', async () => { const engine = new LiquidUMD({ strictVariables: true, diff --git a/test/integration/liquid/register-filters.spec.ts b/test/integration/liquid/register-filters.spec.ts index 54a24751c..380fb0d02 100644 --- a/test/integration/liquid/register-filters.spec.ts +++ b/test/integration/liquid/register-filters.spec.ts @@ -60,4 +60,30 @@ describe('liquid#registerFilter()', function () { return expect(html).toBe(dst) }) }) + + describe('filter name must not inherit from Object.prototype', () => { + it('should treat valueOf as unregistered (no FilterImpl leak)', async () => { + const out = await liquid.parseAndRender( + '{% assign r = 1 | valueOf %}{{ r.liquid.options.fs.sep }}|{{ r }}' + ) + expect(out).toBe('|1') + }) + it('should not expose context, liquid, or token via valueOf', async () => { + const out = await liquid.parseAndRender( + '{% assign r = 1 | valueOf %}{{ r.context }}/{{ r.liquid }}/{{ r.token }}' + ) + expect(out).toBe('//') + }) + it.each(['toString', 'constructor', 'hasOwnProperty', 'isPrototypeOf', '__proto__', '__defineGetter__'])( + 'should treat %s as unregistered filter', + async (name) => { + const out = await liquid.parseAndRender(`{{ "x" | ${name} }}`) + expect(out).toBe('x') + } + ) + it('should throw under strictFilters for valueOf', async () => { + const strict = new Liquid({ strictFilters: true }) + await expect(strict.parseAndRender('{{ 1 | valueOf }}')).rejects.toThrow('undefined filter: valueOf') + }) + }) }) diff --git a/test/integration/liquid/register-tags.spec.ts b/test/integration/liquid/register-tags.spec.ts index f0c85facf..15fddce65 100644 --- a/test/integration/liquid/register-tags.spec.ts +++ b/test/integration/liquid/register-tags.spec.ts @@ -38,4 +38,14 @@ describe('liquid#registerTag()', function () { }) return expect(html).toBe('ABC') }) + + describe('tag name must not inherit from Object.prototype', () => { + it.each(['constructor', 'toString', 'valueOf', 'hasOwnProperty', '__proto__'])( + 'should report %s as unknown tag', + (name) => { + const l = new Liquid() + expect(() => l.parse(`{% ${name} %}`)).toThrow(`tag "${name}" not found`) + } + ) + }) }) diff --git a/test/integration/liquid/security.spec.ts b/test/integration/liquid/security.spec.ts deleted file mode 100644 index 23696aefd..000000000 --- a/test/integration/liquid/security.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Liquid } from '../../../src/liquid' - -describe('security', () => { - describe('Object.prototype filter names', () => { - // Regression: `{{ 1 | valueOf }}` used to resolve to Object.prototype.valueOf - // and, when invoked as a filter handler, return the FilterImpl `this` — leaking - // `context`, `liquid`, options, the parser, the loader, etc., enabling RCE. - it('should treat valueOf as an unregistered filter (identity)', async () => { - const liquid = new Liquid() - const out = await liquid.parseAndRender('{% assign r = 1 | valueOf %}{{ r.liquid.options.fs.sep }}|{{ r }}') - expect(out).toBe('|1') - }) - it('should not leak FilterImpl via valueOf', async () => { - const liquid = new Liquid() - const out = await liquid.parseAndRender('{% assign r = 1 | valueOf %}{{ r.context }}/{{ r.liquid }}/{{ r.token }}') - expect(out).toBe('//') - }) - it.each(['toString', 'constructor', 'hasOwnProperty', 'isPrototypeOf', '__proto__', '__defineGetter__'])( - 'should treat %s as an unregistered filter', - async (name) => { - const liquid = new Liquid() - const out = await liquid.parseAndRender(`{{ "x" | ${name} }}`) - expect(out).toBe('x') - } - ) - it('should throw under strictFilters for inherited method names', async () => { - const liquid = new Liquid({ strictFilters: true }) - await expect(liquid.parseAndRender('{{ 1 | valueOf }}')).rejects.toThrow('undefined filter: valueOf') - }) - }) - describe('Object.prototype tag names', () => { - // Regression: `{% constructor %}` used to resolve to Object via tags['constructor'], - // bypassing the "tag not found" assertion and crashing later with a confusing error. - it.each(['constructor', 'toString', 'valueOf', 'hasOwnProperty', '__proto__'])( - 'should report %s as unknown tag', - (name) => { - const liquid = new Liquid() - expect(() => liquid.parse(`{% ${name} %}`)).toThrow(`tag "${name}" not found`) - } - ) - }) -})