diff --git a/src/infra/ed25519-signature.test.ts b/src/infra/ed25519-signature.test.ts new file mode 100644 index 000000000000..4fcb1c683b0e --- /dev/null +++ b/src/infra/ed25519-signature.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { base64UrlDecode, base64UrlEncode } from "./ed25519-signature.ts"; + +describe("base64UrlDecode", () => { + it("decodes a fixed-size ed25519 public key without throwing", () => { + const key = base64UrlEncode(Buffer.alloc(32, 7)); + expect(base64UrlDecode(key).length).toBe(32); + }); + + it("throws on input exceeding the maximum allowed length", () => { + // MAX_BASE64URL_DECODE_INPUT_LENGTH is 4096; 5000 is safely over it. + const oversized = "A".repeat(5000); + expect(() => base64UrlDecode(oversized)).toThrow(/maximum allowed length/); + }); +}); diff --git a/src/infra/ed25519-signature.ts b/src/infra/ed25519-signature.ts index 836e8fa6835b..e31d4ff1e8d3 100644 --- a/src/infra/ed25519-signature.ts +++ b/src/infra/ed25519-signature.ts @@ -7,7 +7,15 @@ export function base64UrlEncode(buf: Buffer): string { return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, ""); } +// Ed25519 public keys and signatures are fixed-size (<= ~86 base64url chars), +// so a caller passing far larger input is almost certainly malformed or abusive. +// Bound the decoded buffer to keep a single request from allocating arbitrary memory. +const MAX_BASE64URL_DECODE_INPUT_LENGTH = 4096; + export function base64UrlDecode(input: string): Buffer { + if (input.length > MAX_BASE64URL_DECODE_INPUT_LENGTH) { + throw new Error("base64url input exceeds the maximum allowed length"); + } const normalized = input.replaceAll("-", "+").replaceAll("_", "/"); const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4); return Buffer.from(padded, "base64");