Skip to content

fix(auth): return null from getItemAsync on JSON parse failure#2312

Merged
mandarini merged 1 commit into
developfrom
fix/getitemasync-null-on-corrupt-json
May 7, 2026
Merged

fix(auth): return null from getItemAsync on JSON parse failure#2312
mandarini merged 1 commit into
developfrom
fix/getitemasync-null-on-corrupt-json

Conversation

@mandarini

@mandarini mandarini commented May 4, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes a TypeError: Cannot create property 'user' on string ... that crashes _recoverAndRefresh when storage holds a non-JSON value, and stops the raw access token from leaking into error logs as part of that exception message.

What changed?

getItemAsync now returns null when JSON.parse throws, instead of returning the raw string.

  • packages/core/auth-js/src/lib/helpers.ts: change the catch branch to return null and add a comment explaining the invariant that storage in this code path is always JSON encoded by setItemAsync.
  • packages/core/auth-js/test/helpers.test.ts: add a getItemAsync describe block with five cases covering missing value, empty string, valid JSON object, unparseable JSON (the corrupted chunked cookie scenario), and JSON encoded primitive.

Why was this change needed?

Reported in supabase/ssr#169 with full root-cause analysis from the reporter.

When a session is large enough to span 3+ cookie chunks (sb-...auth-token.0/.1/.2/...) and a server side refresh writes only some of the new chunks (e.g. response committed before all Set-Cookie headers go out, or partial set/remove during SSR), the browser ends up with a mix of chunks from different generations. @supabase/ssr joins them in combineChunks with no integrity check, and the combined payload either fails to base64url decode or decodes to bytes that fail JSON.parse.

Before this change, getItemAsync caught that JSON.parse error and returned the raw string. _recoverAndRefresh (GoTrueClient.ts:4544-4582) then entered the else if (currentSession && !currentSession.user) branch and tried currentSession.user = userNotAvailableProxy() on a string primitive, throwing the TypeError. The _isValidSession guard at line 4587 runs after the crash site, so it never gets a chance to clear the bad data. As a side effect, the TypeError message embeds the raw session JSON (with access_token), which means the token ends up in framework error logs and any error reporting service the app sends to.

The bug is also self reinforcing: if the corrupted string is later persisted via setItemAsync, it gets double JSON encoded (the raw string wrapped in extra quotes), and the user stays broken until they manually clear cookies.

Storage in this code path is always written as JSON. storage.setItem is only called from setItemAsync in this package (confirmed by grep), and setItemAsync always runs JSON.stringify. So a parse failure unambiguously means the entry is corrupted, and treating it as absent is the only correct interpretation. Returning null:

  • prevents the TypeError (currentSession becomes null, _isValidSession rejects cleanly).
  • prevents the self reinforcing double encoding (no string ever flows through _recoverAndRefresh to _saveSession).
  • prevents the token leakage (no exception is built, so no token in the error message).

Refs supabase/ssr#169. A companion defense in depth fix in @supabase/ssr validates chunked cookie integrity before the value reaches this code path; that change is going up in a separate PR.

Breaking changes

  • This PR contains no breaking changes

The only behavior change is that getItemAsync now returns null instead of a non-JSON string when JSON.parse fails. Callers in this package treat the result as Session | null or { user: User | null } | null and never expected a raw string in that path. The PKCE code verifier read at GoTrueClient.ts:1850 already coerces storageItem ?? '' and .split('/') an empty string ends up triggering AuthPKCECodeVerifierMissingError cleanly, which is the desired behavior for a corrupted verifier anyway.

Checklist

  • I have read the Contributing Guidelines
  • My PR title follows the conventional commit format: <type>(<scope>): <description>
  • I have run npx nx format to ensure consistent code formatting
  • I have added tests for new functionality (if applicable)
  • I have updated documentation (if applicable)

Additional notes

This is the primary fix for the user visible crash on supabase/ssr#169. A separate PR on supabase/ssr adds defense in depth so corrupted chunks never reach auth-js in the first place; that one also covers a related symptom seen in production (Invalid Base64-URL character "." at position N when mixed chunks happen to contain a raw JWT separator).

@mandarini mandarini requested review from a team as code owners May 4, 2026 11:20
@github-actions github-actions Bot added the auth-js Related to the auth-js library. label May 4, 2026
@pkg-pr-new

pkg-pr-new Bot commented May 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

@supabase/auth-js

npm i https://pkg.pr.new/@supabase/auth-js@2312

@supabase/functions-js

npm i https://pkg.pr.new/@supabase/functions-js@2312

@supabase/postgrest-js

npm i https://pkg.pr.new/@supabase/postgrest-js@2312

@supabase/realtime-js

npm i https://pkg.pr.new/@supabase/realtime-js@2312

@supabase/storage-js

npm i https://pkg.pr.new/@supabase/storage-js@2312

@supabase/supabase-js

npm i https://pkg.pr.new/@supabase/supabase-js@2312

commit: 102500a

Comment thread packages/core/auth-js/src/lib/helpers.ts
mandarini added a commit to supabase/ssr that referenced this pull request May 7, 2026
)

## Summary

Adds defensive validation to chunked cookie decoding so corrupted or
mid-write cookie state no longer propagates a malformed string into
`@supabase/auth-js`. When a value is read via the chunked cookie path
and carries the `base64-` prefix written by this module, the decoded
payload is now verified to be valid JSON (which it always must be, since
`setItemAsync` in auth-js writes via `JSON.stringify`). On decode or
parse failure we log a console warning and return `null`, signalling the
entry is absent so the SDK can recover cleanly instead of crashing or
re-saving garbage.

## What changed

- New `decodeChunkedCookieValue` helper in `src/cookies.ts`, used by
both the browser and server `getItem` paths inside
`createStorageFromOptions`.
- The helper:
- Returns the value unchanged when there is no `base64-` prefix (raw
cookies and PKCE code verifier paths are unaffected).
- Catches throws from `stringFromBase64URL` and warns: "could not
base64url-decode chunked cookie value..."
- Validates the decoded value parses as JSON; on failure warns: "chunked
cookie decoded to invalid JSON..."
  - Returns `null` in either failure case.
- Both `getItem` paths in `createStorageFromOptions` now delegate to
this helper.
- Four new tests in `src/cookies.spec.ts` covering: invalid JSON in
decoded chunks, valid JSON in decoded chunks, undecodable base64url
payload, and that raw (non-base64) cookies are passed through without
JSON validation.

## Why

Reported in #169 with a complete root cause analysis from the original
reporter. When a session is large enough to span 3+ cookie chunks
(`sb-...auth-token.0/.1/.2/...`) and a server side refresh writes only
some of the new chunks (e.g. response committed before all `Set-Cookie`
headers go out, partial set/remove during SSR, browser racing concurrent
navigations), the browser ends up holding chunks from two different
generations. `combineChunks` joins them blindly and there are two
failure modes downstream:

1. Combined bytes happen to all fall in the base64url alphabet.
`stringFromBase64URL` succeeds, but the decoded result is garbage that
fails `JSON.parse` later in auth-js. Before the auth-js companion fix,
this produced a `TypeError: Cannot create property 'user' on string ...`
in `_recoverAndRefresh` and embedded the access token JSON in the error
message (token leaked into application error logs).
2. Combined bytes contain a character that is invalid in base64url, e.g.
a `.` from a raw JWT segment that landed in a chunk.
`stringFromBase64URL` throws synchronously: `Invalid Base64-URL
character "." at position N`. This surfaces as an unhandled rejection in
production and was reported separately by another customer hitting the
same root cause class.

This change catches both failure modes at the source so they never reach
auth-js. The corresponding auth-js change
(supabase/supabase-js#2312) is the primary fix
for the user visible crash from variant 1; this PR is defense in depth
and is the actual fix for variant 2.

## Notes for reviewers

- The JSON validation is intentionally narrow: it only runs when the
value carries the `base64-` prefix that this module itself writes. Raw
cookies (`cookieEncoding: "raw"`), and any cookie value that does not
carry the prefix, are returned unchanged. This keeps the change safe for
users storing non-JSON values via the storage interface and for the PKCE
code verifier path.
- The helper is reused by both the browser and server `getItem`. The
server path retained its existing `typeof chunkedCookie !== "string"`
defensive shortcut for cases where `combineChunks` might return a
non-string at runtime.

## Tracking

- Closes #169
- Closes #87
- Refs supabase/supabase-js#2312 (the auth-js
companion fix that prevents the TypeError on the path before this PR's
defenses)
@mandarini mandarini requested a review from o-santi May 7, 2026 12:15
@mandarini mandarini merged commit 4724b13 into develop May 7, 2026
25 checks passed
@mandarini mandarini deleted the fix/getitemasync-null-on-corrupt-json branch May 7, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth-js Related to the auth-js library.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants