-
Notifications
You must be signed in to change notification settings - Fork 136
fix(vc): isExpired fails closed on an unparseable expirationDate #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| "@agentcommercekit/vc": patch | ||
| --- | ||
|
|
||
| Fix `isExpired` failing open on an unparseable `expirationDate` | ||
|
|
||
| `isExpired` returned `false` (not expired) whenever `credential.expirationDate` | ||
| was present but could not be parsed into a valid date. Since `isExpired` is | ||
| the check `verifyParsedCredential` uses to reject expired credentials, a | ||
| credential with a malformed or malicious `expirationDate` value was treated | ||
| as never-expiring instead of being rejected. | ||
|
|
||
| `isExpired` now fails closed: an unparseable `expirationDate` is treated as | ||
| expired, matching the safer default for a security-relevant check. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,19 +3,37 @@ import type { W3CCredential } from "../types" | |
| /** | ||
| * Check if a credential is expired | ||
| * | ||
| * Fails closed: a credential with an `expirationDate` that is present but | ||
| * cannot be parsed as a valid date is treated as expired, not as | ||
| * non-expiring. | ||
| * | ||
| * @param credential - The {@link W3CCredential} to check | ||
| * @returns `true` if the credential is expired, `false` otherwise | ||
| * @returns `true` if the credential is expired (or has an unparseable | ||
| * expiration date), `false` otherwise | ||
| */ | ||
| export function isExpired(credential: W3CCredential): boolean { | ||
| if (!credential.expirationDate) { | ||
| if (credential.expirationDate === undefined) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The guard only excludes The security gain is small since a valid far-future string does the same. Potential fix: return |
||
| return false | ||
| } | ||
|
|
||
| // `parseJwtCredential` does not validate that `expirationDate` is a | ||
| // string, so a malformed or malicious credential could carry a number | ||
| // (which `Date()` accepts as epoch milliseconds) or another non-string | ||
| // JSON value here. Reject anything that isn't a string outright, rather | ||
| // than letting it reach `new Date()`, which would silently accept types | ||
| // the {@link W3CCredential} type only documents as a string. | ||
| if (typeof credential.expirationDate !== "string") { | ||
| return true | ||
| } | ||
|
|
||
| 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: an | ||
| // unparseable expiration date must not be treated as "never expires", | ||
| // since that would let a malformed or malicious `expirationDate` value | ||
| // grant a credential unbounded validity. | ||
| return true | ||
| } | ||
|
|
||
| return expirationDate < new Date() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -411,9 +411,12 @@ async function resolveStatusListCredential( | |
| ) | ||
| } | ||
|
|
||
| // Check the expiry directly rather than through `isExpired`, which reads an | ||
| // unparseable date as "not expired". The expiry is the main bound on status | ||
| // list replay, so a malformed one must not quietly remove it. | ||
| // Check the expiry directly rather than through `isExpired`: this path | ||
| // must throw `undetermined()` with a URL-specific message (isExpired only | ||
| // returns a boolean), and must reject a list that expires at exactly `now` | ||
| // (`<=`), whereas isExpired's own bound is strict (`<`). The expiry is the | ||
| // main bound on status list replay, so a malformed one must not quietly | ||
| // remove it. | ||
| if (verified.expirationDate !== undefined) { | ||
| const expiresAt = Date.parse(verified.expirationDate) | ||
|
Comment on lines
420
to
421
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: printf '%s\n' '--- target context ---'
sed -n '380,445p' packages/vc/src/verification/is-revoked.ts
printf '%s\n' '--- expiry helper references ---'
rg -n -C 5 'function isExpired|const isExpired|isExpired\\(' packages/vc/src/verification packages/vc/src
printf '%s\n' '--- ECMAScript Date.parse coercion probe ---'
node - <<'JS'
const values = [
["9999-01-01T00:00:00.000Z"],
{ toString() { return "9999-01-01T00:00:00.000Z"; } },
[null],
[""],
];
for (const value of values) {
const parsed = Date.parse(value);
console.log(JSON.stringify(value), parsed, Number.isNaN(parsed), parsed > Date.now());
}
JSRepository: agentcommercekit/ack Length of output: 3081 🏁 Script executed: printf '%s\n' '--- expiry helper ---'
rg -n -C 8 'isExpired' packages/vc/src/verification packages/vc/src --glob '*.ts' --glob '*.tsx'
printf '%s\n' '--- expirationDate declarations and proof path ---'
rg -n -C 5 'expirationDate|verifyStatusListProof|verifyCredential' packages/vc/src packages/did/src --glob '*.ts' --glob '*.tsx'Repository: agentcommercekit/ack Length of output: 50376 🏁 Script executed: printf '%s\n' '--- verifyStatusListProof definition ---'
rg -n -C 20 'verifyStatusListProof' packages/vc/src/verification/is-revoked.ts packages/vc/src/verification/verify-proof.ts packages/vc/src --glob '*.ts'
printf '%s\n' '--- proof parser implementation references ---'
rg -n -C 8 'parseJwtCredential|decode.*credential|expirationDate' packages/vc/src/verification/verify-proof.ts packages/vc/src/verification/is-revoked.ts packages/vc/src/types.ts --glob '*.ts'Repository: agentcommercekit/ack Length of output: 20628 Authorization Bypass (CWE-20): Improper Input Validation Reachability: External · Exploitability: Difficult Reject non-string expiration dates in the status-list path. Check that 🤖 Prompt for AI AgentsSource: MCP tools |
||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.