fix(core): revive date strings in safeJSONParse for pre-parsed objects#8248
fix(core): revive date strings in safeJSONParse for pre-parsed objects#8248
Conversation
When a Redis client (or similar) returns already-parsed JSON objects, safeJSONParse previously returned them as-is without converting ISO 8601 date strings to Date instances. This caused silent failures in date comparisons like `expiresAt < new Date()` across 40+ call sites, since string-vs-Date comparison coerces to NaN and always returns false — meaning expired tokens were never detected as expired. Split the reviver into two functions: - reviveDate: leaf-level converter for JSON.parse reviver - reviveDates: recursive walker for pre-parsed objects Both paths now produce consistent Date objects for ISO 8601 strings.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
@better-auth/api-key
better-auth
auth
@better-auth/core
@better-auth/drizzle-adapter
@better-auth/electron
@better-auth/expo
@better-auth/i18n
@better-auth/kysely-adapter
@better-auth/memory-adapter
@better-auth/mongo-adapter
@better-auth/oauth-provider
@better-auth/passkey
@better-auth/prisma-adapter
@better-auth/redis-storage
@better-auth/scim
@better-auth/sso
@better-auth/stripe
@better-auth/telemetry
@better-auth/test-utils
commit: |
There was a problem hiding this comment.
Pull request overview
This PR fixes safeJSONParse so ISO 8601 date strings are revived into Date instances not only when parsing JSON strings, but also when the input is already a parsed object (e.g., from Redis clients that auto-parse JSON). This prevents incorrect expiry comparisons at call sites that assume expiresAt is a Date.
Changes:
- Added recursive date revival for non-string (already-parsed) inputs to
safeJSONParse. - Refactored date revival logic into reusable helpers (
reviveDate/reviveDates). - Added tests covering date revival behavior when secondary storage returns pre-parsed objects.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
packages/core/src/utils/json.ts |
Extends safeJSONParse to recursively revive ISO date strings to Date for already-parsed objects/arrays. |
packages/better-auth/src/db/internal-adapter.test.ts |
Adds test coverage for date revival when secondary storage returns pre-parsed objects. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (typeof value === "object") { | ||
| const result: Record<string, any> = {}; | ||
| for (const key of Object.keys(value)) { | ||
| result[key] = reviveDates((value as Record<string, any>)[key]); | ||
| } | ||
| return result; |
There was a problem hiding this comment.
reviveDates builds a plain {} and assigns arbitrary keys from untrusted input. If a payload includes __proto__ (or similar magic keys), result[key] = ... can mutate the returned object’s prototype, enabling prototype-pollution-style behavior. Consider using a null-prototype object (e.g. Object.create(null)) and/or defining properties in a way that treats __proto__ as a normal key, or explicitly blocking these keys during the copy.
| set(key: string, value: string, ttl?: number) { | ||
| // Store as pre-parsed object (simulating Redis auto-parse) | ||
| dataMap.set(key, JSON.parse(value)); | ||
| if (ttl) ttlMap.set(key, ttl); |
There was a problem hiding this comment.
In this test helper, if (ttl) ttlMap.set(key, ttl); will skip storing TTL when ttl is 0 (because of the truthiness check). Using an explicit undefined check (e.g. ttl !== undefined) makes the helper behave correctly for all numeric TTL values.
| if (ttl) ttlMap.set(key, ttl); | |
| if (ttl !== undefined) ttlMap.set(key, ttl); |
…rialized JSON) Add tests covering the scenario where secondaryStorage (e.g. @vercel/kv) returns already-parsed objects instead of strings, causing expiresAt to be a string rather than a Date. The root cause was fixed in #8248 via reviveDates in safeJSONParse. These tests prevent regression.
Summary
safeJSONParseto convert ISO 8601 date strings toDateinstances when the input is an already-parsed object (e.g., from Redis clients that auto-parse JSON)JSON.parsestring input — pre-parsed objects were returned as-is with date fields still as stringsexpiresAt < new Date(), sincestring < Datecoerces toNaNand always returnsfalse— expired tokens were never detected as expiredTest plan
internal-adapter.test.tsunder"safeJSONParse date revival in secondary storage":findVerificationValuereturnsDateinstances when storage returns pre-parsed objectsexpiresAt > new Date())Dateobjects are consistent across multiple readspnpm typecheck,pnpm lint,pnpm buildall pass