From f4bfd03b4902a271aff878ab4eec687fa94c1f7e Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Tue, 12 May 2026 01:03:40 +0800 Subject: [PATCH] test: assert null-prototype registries vs all Object.prototype keys Co-authored-by: Cursor --- test/e2e/issues.spec.ts | 15 ++++---- .../liquid/register-filters.spec.ts | 37 +++++++++---------- test/integration/liquid/register-tags.spec.ts | 16 +++++--- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/test/e2e/issues.spec.ts b/test/e2e/issues.spec.ts index fea07b3cd..35d3f17c3 100644 --- a/test/e2e/issues.spec.ts +++ b/test/e2e/issues.spec.ts @@ -77,15 +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 }}' + it('filter/tag maps are null-prototype; built-ins work (node + UMD)', async () => { + const tpl = `{{ 'a' | append: 'b' }}` 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') + expect(Object.getPrototypeOf(nodeEngine.filters)).toBeNull() + expect(Object.getPrototypeOf(nodeEngine.tags)).toBeNull() + expect(Object.getPrototypeOf(umdEngine.filters)).toBeNull() + expect(Object.getPrototypeOf(umdEngine.tags)).toBeNull() + expect(await nodeEngine.parseAndRender(tpl)).toBe('ab') + expect(await umdEngine.parseAndRender(tpl)).toBe('ab') }) it('lenientIf not working as expected in umd #313', async () => { const engine = new LiquidUMD({ diff --git a/test/integration/liquid/register-filters.spec.ts b/test/integration/liquid/register-filters.spec.ts index 380fb0d02..33a46d295 100644 --- a/test/integration/liquid/register-filters.spec.ts +++ b/test/integration/liquid/register-filters.spec.ts @@ -61,29 +61,28 @@ describe('liquid#registerFilter()', function () { }) }) - 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') + describe('filter registry storage', () => { + it('should use a null-prototype map for filters', () => { + expect(Object.getPrototypeOf(liquid.filters)).toBeNull() }) - 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('should still resolve built-in filters', async () => { + expect(await liquid.parseAndRender(`{{ 'a' | append: 'b' }}`)).toBe('ab') }) - 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 not resolve names that exist only on Object.prototype', async () => { + const registered = new Set(Object.keys(liquid.filters)) + for (const name of Object.getOwnPropertyNames(Object.prototype)) { + if (registered.has(name)) continue + const out = await liquid.parseAndRender(`{{ x | ${name} }}`, { x: 42 }) + expect(out).toBe('42') } - ) - it('should throw under strictFilters for valueOf', async () => { + }) + it('should reject unknown filter names under strictFilters, including Object.prototype keys', async () => { const strict = new Liquid({ strictFilters: true }) - await expect(strict.parseAndRender('{{ 1 | valueOf }}')).rejects.toThrow('undefined filter: valueOf') + const registered = new Set(Object.keys(strict.filters)) + for (const name of Object.getOwnPropertyNames(Object.prototype)) { + if (registered.has(name)) continue + await expect(strict.parseAndRender(`{{ 1 | ${name} }}`)).rejects.toThrow('undefined filter') + } }) }) }) diff --git a/test/integration/liquid/register-tags.spec.ts b/test/integration/liquid/register-tags.spec.ts index 15fddce65..b520a66da 100644 --- a/test/integration/liquid/register-tags.spec.ts +++ b/test/integration/liquid/register-tags.spec.ts @@ -39,13 +39,17 @@ 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() + describe('tag registry storage', () => { + it('should use a null-prototype map for tags', () => { + expect(Object.getPrototypeOf(new Liquid().tags)).toBeNull() + }) + it('should not resolve names that exist only on Object.prototype', () => { + const l = new Liquid() + const registered = new Set(Object.keys(l.tags)) + for (const name of Object.getOwnPropertyNames(Object.prototype)) { + if (registered.has(name)) continue expect(() => l.parse(`{% ${name} %}`)).toThrow(`tag "${name}" not found`) } - ) + }) }) })