From 40d750f9a03d9d7c871f36912fe83ba7b80591ca Mon Sep 17 00:00:00 2001 From: Sertug17 <104278804+Sertug17@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:58:16 +0300 Subject: [PATCH] fix: validate decimals and fail-closed on unparseable expirationDate --- packages/ack-pay/src/payment-request.test.ts | 13 +++++++++++++ packages/ack-pay/src/schemas/valibot.ts | 2 +- packages/vc/src/verification/is-expired.test.ts | 11 +++++++++-- packages/vc/src/verification/is-expired.ts | 9 ++++++--- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/ack-pay/src/payment-request.test.ts b/packages/ack-pay/src/payment-request.test.ts index 018c78b3..4846165c 100644 --- a/packages/ack-pay/src/payment-request.test.ts +++ b/packages/ack-pay/src/payment-request.test.ts @@ -40,4 +40,17 @@ describe("isPaymentRequest", () => { it("returns false if given a non-object", () => { expect(isPaymentRequest(1)).toBe(false) }) + + it("returns false when decimals is negative", () => { + // decimals uses minValue(0) — a validation that rejects negative values. + // The previous toMinValue(0) silently clamped -6 to 0 instead of rejecting it. + expect( + isPaymentRequest({ + ...validPaymentRequest, + paymentOptions: [ + { ...validPaymentRequest.paymentOptions[0], decimals: -6 }, + ], + }), + ).toBe(false) + }) }) diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index 06a58f1e..dfb5bcc7 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -7,7 +7,7 @@ const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema]) export const paymentOptionSchema = v.object({ id: v.string(), amount: v.union([v.pipe(v.number(), v.integer(), v.gtValue(0)), v.string()]), - decimals: v.pipe(v.number(), v.integer(), v.toMinValue(0)), + decimals: v.pipe(v.number(), v.integer(), v.minValue(0)), currency: v.string(), recipient: v.string(), network: v.optional(v.string()), diff --git a/packages/vc/src/verification/is-expired.test.ts b/packages/vc/src/verification/is-expired.test.ts index ac456e47..a2df717c 100644 --- a/packages/vc/src/verification/is-expired.test.ts +++ b/packages/vc/src/verification/is-expired.test.ts @@ -47,9 +47,16 @@ describe("isExpired", () => { expect(isExpired(credential)).toBe(false) }) - it("handles invalid date strings gracefully", () => { + it("returns true for an unparseable expiration date (fail-closed)", () => { const credential = buildCredential("invalid-date") + // An unparseable date must not silently pass as not expired; failing + // closed is safer than failing open for a security-critical check. + expect(isExpired(credential)).toBe(true) + }) - expect(isExpired(credential)).toBe(false) + it("returns true for an empty-string expiration date (fail-closed)", () => { + const credential = buildCredential("") + // Empty string is present but unparseable — fail closed. + expect(isExpired(credential)).toBe(true) }) }) diff --git a/packages/vc/src/verification/is-expired.ts b/packages/vc/src/verification/is-expired.ts index 1d66ac8e..b4228065 100644 --- a/packages/vc/src/verification/is-expired.ts +++ b/packages/vc/src/verification/is-expired.ts @@ -7,15 +7,18 @@ import type { W3CCredential } from "../types" * @returns `true` if the credential is expired, `false` otherwise */ export function isExpired(credential: W3CCredential): boolean { - if (!credential.expirationDate) { + if (credential.expirationDate === undefined) { return false } const expirationDate = new Date(credential.expirationDate) if (isNaN(expirationDate.getTime())) { - // Expiration date is invalid, so we consider the credential not expired - return false + // Expiration date is present but unparseable — fail closed and treat as + // expired. Returning false (not expired) would allow a credential with a + // garbage date to pass all downstream checks, which is the wrong default + // for a security-critical guard. + return true } return expirationDate < new Date()