fix(auth): return null from getItemAsync on JSON parse failure#2312
Merged
Conversation
@supabase/auth-js
@supabase/functions-js
@supabase/postgrest-js
@supabase/realtime-js
@supabase/storage-js
@supabase/supabase-js
commit: |
o-santi
reviewed
May 6, 2026
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)
grdsdev
approved these changes
May 7, 2026
This was referenced May 11, 2026
This was referenced May 13, 2026
This was referenced May 13, 2026
This was referenced May 14, 2026
This was referenced May 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes a
TypeError: Cannot create property 'user' on string ...that crashes_recoverAndRefreshwhen 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?
getItemAsyncnow returnsnullwhenJSON.parsethrows, instead of returning the raw string.packages/core/auth-js/src/lib/helpers.ts: change thecatchbranch to returnnulland add a comment explaining the invariant that storage in this code path is always JSON encoded bysetItemAsync.packages/core/auth-js/test/helpers.test.ts: add agetItemAsyncdescribe 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 allSet-Cookieheaders go out, or partial set/remove during SSR), the browser ends up with a mix of chunks from different generations.@supabase/ssrjoins them incombineChunkswith no integrity check, and the combined payload either fails to base64url decode or decodes to bytes that failJSON.parse.Before this change,
getItemAsynccaught thatJSON.parseerror and returned the raw string._recoverAndRefresh(GoTrueClient.ts:4544-4582) then entered theelse if (currentSession && !currentSession.user)branch and triedcurrentSession.user = userNotAvailableProxy()on a string primitive, throwing the TypeError. The_isValidSessionguard 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 (withaccess_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.setItemis only called fromsetItemAsyncin this package (confirmed by grep), andsetItemAsyncalways runsJSON.stringify. So a parse failure unambiguously means the entry is corrupted, and treating it as absent is the only correct interpretation. Returningnull:TypeError(currentSession becomesnull,_isValidSessionrejects cleanly)._recoverAndRefreshto_saveSession).Refs supabase/ssr#169. A companion defense in depth fix in
@supabase/ssrvalidates chunked cookie integrity before the value reaches this code path; that change is going up in a separate PR.Breaking changes
The only behavior change is that
getItemAsyncnow returnsnullinstead of a non-JSON string whenJSON.parsefails. Callers in this package treat the result asSession | nullor{ user: User | null } | nulland never expected a raw string in that path. The PKCE code verifier read atGoTrueClient.ts:1850already coercesstorageItem ?? ''and.split('/')an empty string ends up triggeringAuthPKCECodeVerifierMissingErrorcleanly, which is the desired behavior for a corrupted verifier anyway.Checklist
<type>(<scope>): <description>npx nx formatto ensure consistent code formattingAdditional notes
This is the primary fix for the user visible crash on supabase/ssr#169. A separate PR on
supabase/ssradds 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 Nwhen mixed chunks happen to contain a raw JWT separator).