From e7eb6ff670c8eb05bd678b6fadd266d2d8d649b4 Mon Sep 17 00:00:00 2001 From: Mirochill <200482516+Mirochill@users.noreply.github.com> Date: Sat, 30 May 2026 23:17:49 +0200 Subject: [PATCH] fix: preserve __proto__ during receipt canonicalization --- src/canonicalize.ts | 3 ++- tests/verify.test.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/canonicalize.ts b/src/canonicalize.ts index b45e4a7..0635c5e 100644 --- a/src/canonicalize.ts +++ b/src/canonicalize.ts @@ -53,7 +53,8 @@ function sortKeys(obj: unknown): unknown { return obj.map(sortKeys); } - const sorted: Record = {}; + // Preserve "__proto__" as signed data instead of invoking Object.prototype's setter. + const sorted: Record = Object.create(null) as Record; const keys = Object.keys(obj as Record).sort(); for (const key of keys) { diff --git a/tests/verify.test.ts b/tests/verify.test.ts index 0fd26db..26b5e2f 100644 --- a/tests/verify.test.ts +++ b/tests/verify.test.ts @@ -1,6 +1,8 @@ import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; +import { sign } from 'node:crypto'; import { describe, expect, it } from 'vitest'; +import { canonicalizeReceiptBytes } from '../src/canonicalize.js'; import { verifyReceipt } from '../src/verify.js'; async function loadJson(name: string): Promise { @@ -9,6 +11,7 @@ async function loadJson(name: string): Promise { } const keyFile = resolve(process.cwd(), 'tests/fixtures/public-key.pem'); +const privateKeyFile = resolve(process.cwd(), 'tests/fixtures/private-key.pem'); describe('verifyReceipt', () => { it('verifies a valid receipt', async () => { @@ -59,4 +62,19 @@ describe('verifyReceipt', () => { expect(result.errorCode).toBe('SIGNATURE_INVALID'); } }); + + it('rejects tampering with a nested __proto__ request field', async () => { + const receipt = await loadJson('valid.json') as Record; + const privateKey = await readFile(privateKeyFile, 'utf8'); + receipt.requestJson = JSON.parse('{"action":"deploy:production","__proto__":{"scope":"before"}}'); + receipt.signatureValue = sign(null, canonicalizeReceiptBytes(receipt), privateKey).toString('base64'); + receipt.requestJson = JSON.parse('{"action":"deploy:production","__proto__":{"scope":"after"}}'); + + const result = await verifyReceipt(receipt, { keyFile, noNetwork: true }); + expect(result.verified).toBe(false); + if (!result.verified) { + expect(result.exitCode).toBe(1); + expect(result.errorCode).toBe('SIGNATURE_INVALID'); + } + }); });