From ddf109a683ca38439249cadcece10cace17ca812 Mon Sep 17 00:00:00 2001 From: OWConnoi <123777754+OWConnoi@users.noreply.github.com> Date: Mon, 11 May 2026 22:57:24 +0100 Subject: [PATCH] Validate FLAGS_SECRET length for serialization --- packages/flags/src/lib/serialization.test.ts | 18 ++++++++++++++++++ packages/flags/src/lib/serialization.ts | 18 ++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 packages/flags/src/lib/serialization.test.ts diff --git a/packages/flags/src/lib/serialization.test.ts b/packages/flags/src/lib/serialization.test.ts new file mode 100644 index 00000000..3e091026 --- /dev/null +++ b/packages/flags/src/lib/serialization.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import { deserialize, serialize } from './serialization'; + +const invalidSecret = 'short'; + +describe('serialization secret validation', () => { + it('rejects signing with a secret that is not 32 bytes', async () => { + await expect( + serialize({ feature: true }, [{ key: 'feature' }], invalidSecret), + ).rejects.toThrow('flags: Invalid secret'); + }); + + it('rejects verification with a secret that is not 32 bytes', async () => { + await expect( + deserialize('invalid.code', [{ key: 'feature' }], invalidSecret), + ).rejects.toThrow('flags: Invalid secret'); + }); +}); diff --git a/packages/flags/src/lib/serialization.ts b/packages/flags/src/lib/serialization.ts index 8dc9ec78..b47b810a 100644 --- a/packages/flags/src/lib/serialization.ts +++ b/packages/flags/src/lib/serialization.ts @@ -3,6 +3,8 @@ import type { JsonValue } from '..'; import type { FlagOption } from '../types'; import { memoizeOne } from './async-memoize-one'; +const SECRET_BYTE_LENGTH = 32; + // 252 max options length allows storing index 0 to 251, // so 252 is the first SPECIAL_INTEGER export const MAX_OPTION_LENGTH = 252; @@ -15,9 +17,21 @@ enum SPECIAL_INTEGERS { UNLISTED_VALUE = 255, } +function getSecretKey(secret: string): Uint8Array { + const encodedSecret = base64url.decode(secret); + + if (encodedSecret.length !== SECRET_BYTE_LENGTH) { + throw new Error( + 'flags: Invalid secret, it must be a 256-bit key (32 bytes)', + ); + } + + return encodedSecret; +} + const memoizedVerify = memoizeOne( (code: string, secret: string) => - compactVerify(code, base64url.decode(secret), { + compactVerify(code, getSecretKey(secret), { algorithms: ['HS256'], }), (a, b) => a[0] === b[0] && a[1] === b[1], // only first two args matter @@ -28,7 +42,7 @@ const memoizedSign = memoizeOne( (uint8Array: Uint8Array, secret) => new CompactSign(uint8Array) .setProtectedHeader({ alg: 'HS256' }) - .sign(base64url.decode(secret)), + .sign(getSecretKey(secret)), (a, b) => // matchedIndices array must be equal a[0].length === b[0].length &&