From 46db48cc9d0e5627c7373f0f072165c971c46272 Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Sat, 8 Aug 2026 16:21:41 +0000 Subject: [PATCH 1/3] 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, From 518f7e953f556eaa6183e0d507d585e9f75ccb97 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Fri, 14 Aug 2026 01:23:48 +0700 Subject: [PATCH 2/3] Add changeset for SIWE fail-closed fix --- .changeset/siwe-fail-closed-invalid-dates.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/siwe-fail-closed-invalid-dates.md diff --git a/.changeset/siwe-fail-closed-invalid-dates.md b/.changeset/siwe-fail-closed-invalid-dates.md new file mode 100644 index 00000000000..0268645ae7d --- /dev/null +++ b/.changeset/siwe-fail-closed-invalid-dates.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Reject SIWE login payloads with an unparseable Not Before or Expiration Time instead of skipping the time-bound checks. From 0e5b6b3492383a496ef85c65a6b043933b3f228e Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Fri, 14 Aug 2026 02:02:32 +0700 Subject: [PATCH 3/3] Format verify-login-payload files --- .../auth/core/verify-login-payload.test.ts | 128 +++++++++--------- 1 file changed, 64 insertions(+), 64 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 3456fe8a095..1779e03faa3 100644 --- a/packages/thirdweb/src/auth/core/verify-login-payload.test.ts +++ b/packages/thirdweb/src/auth/core/verify-login-payload.test.ts @@ -147,76 +147,76 @@ describe("verifyLoginPayload", () => { }); }); - 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"; - }, +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, - }); + 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 verificationResult = await verifyLoginPayload(options)(signatureResult); - expect(verificationResult.valid).toBe(false); - if (!verificationResult.valid) { - expect(verificationResult.error).toMatch(/invalid Expiration Time/i); - } + const signatureResult = await signLoginPayload({ + account: TEST_ACCOUNT_A, + payload: payloadToSign, }); - 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"; - }, + 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, - }); + 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 verificationResult = await verifyLoginPayload(options)(signatureResult); - expect(verificationResult.valid).toBe(false); - if (!verificationResult.valid) { - expect(verificationResult.error).toMatch(/invalid Not Before/i); - } + 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); + } +});