From 46db48cc9d0e5627c7373f0f072165c971c46272 Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Sat, 8 Aug 2026 16:21:41 +0000 Subject: [PATCH] fix(auth): fail closed on unparseable SIWE Not Before / Expiration Invalid Date comparisons are always false in JS, so unparseable invalid_before / expiration_time previously skipped time bounds while signature verification still ran. Signed-off-by: Sasha Mitchell --- .../auth/core/verify-login-payload.test.ts | 74 +++++++++++++++++++ .../src/auth/core/verify-login-payload.ts | 20 ++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/packages/thirdweb/src/auth/core/verify-login-payload.test.ts b/packages/thirdweb/src/auth/core/verify-login-payload.test.ts index 429f0fd05a3..3456fe8a095 100644 --- a/packages/thirdweb/src/auth/core/verify-login-payload.test.ts +++ b/packages/thirdweb/src/auth/core/verify-login-payload.test.ts @@ -146,3 +146,77 @@ describe("verifyLoginPayload", () => { expect(verificationResult.valid).toBe(false); }); }); + + test("should fail closed on unparseable expiration_time", async () => { + const options = { + client: TEST_CLIENT, + domain: "example.com", + login: { + nonce: { + generate() { + return "20cd4ddb-6857-4d36-8e44-9f6e026b8de9"; + }, + validate(uuid: string) { + return uuid === "20cd4ddb-6857-4d36-8e44-9f6e026b8de9"; + }, + }, + payloadExpirationTimeSeconds: 3600, + uri: "https://example.com", + version: "1.0", + }, + }; + + const generatePayload = generateLoginPayload(options); + const payloadToSign = await generatePayload({ + address: TEST_ACCOUNT_A.address, + }); + payloadToSign.expiration_time = "never"; + + const signatureResult = await signLoginPayload({ + account: TEST_ACCOUNT_A, + payload: payloadToSign, + }); + + const verificationResult = await verifyLoginPayload(options)(signatureResult); + expect(verificationResult.valid).toBe(false); + if (!verificationResult.valid) { + expect(verificationResult.error).toMatch(/invalid Expiration Time/i); + } + }); + + test("should fail closed on unparseable invalid_before", async () => { + const options = { + client: TEST_CLIENT, + domain: "example.com", + login: { + nonce: { + generate() { + return "20cd4ddb-6857-4d36-8e44-9f6e026b8de9"; + }, + validate(uuid: string) { + return uuid === "20cd4ddb-6857-4d36-8e44-9f6e026b8de9"; + }, + }, + payloadExpirationTimeSeconds: 3600, + uri: "https://example.com", + version: "1.0", + }, + }; + + const generatePayload = generateLoginPayload(options); + const payloadToSign = await generatePayload({ + address: TEST_ACCOUNT_A.address, + }); + payloadToSign.invalid_before = ""; + + const signatureResult = await signLoginPayload({ + account: TEST_ACCOUNT_A, + payload: payloadToSign, + }); + + const verificationResult = await verifyLoginPayload(options)(signatureResult); + expect(verificationResult.valid).toBe(false); + if (!verificationResult.valid) { + expect(verificationResult.error).toMatch(/invalid Not Before/i); + } + }); diff --git a/packages/thirdweb/src/auth/core/verify-login-payload.ts b/packages/thirdweb/src/auth/core/verify-login-payload.ts index 6eaac5b5880..ac251051fcb 100644 --- a/packages/thirdweb/src/auth/core/verify-login-payload.ts +++ b/packages/thirdweb/src/auth/core/verify-login-payload.ts @@ -100,14 +100,30 @@ export function verifyLoginPayload(options: AuthOptions) { const currentDate = new Date(); - if (currentDate < new Date(payload.invalid_before)) { + // Invalid Date comparisons are always false in JS, so unparseable + // invalid_before / expiration_time previously skipped time bounds. + const notBefore = new Date(payload.invalid_before); + if (Number.isNaN(notBefore.getTime())) { + return { + error: "Payload has invalid Not Before time", + valid: false, + }; + } + if (currentDate < notBefore) { return { error: "Payload is not yet valid", valid: false, }; } - if (currentDate > new Date(payload.expiration_time)) { + const expiration = new Date(payload.expiration_time); + if (Number.isNaN(expiration.getTime())) { + return { + error: "Payload has invalid Expiration Time", + valid: false, + }; + } + if (currentDate > expiration) { return { error: "Payload has expired", valid: false,