Replies: 7 comments 8 replies
-
Nice explanation. I was just looking at some code and I do a |
Beta Was this translation helpful? Give feedback.
-
@vickkhera yes, that would mitigate it as well. setSession calls getUser under-the-hood, and then saves a subset of data to /* Reference: https://github.com/supabase/auth-js/blob/master/src/GoTrueClient.ts#L1328-L1334 */
{
access_token, /* Whatever was passed-in to setSession() */
refresh_token, /* Whatever was passed-in to setSession() */
user, /* Based on the getUser() call */
token_type, /* Always "bearer" */
expires_at, /* Calculated from the decoded access_token */
expires_in /* From the decoded access_token */
} |
Beta Was this translation helpful? Give feedback.
-
Thank you for writing this, it's really helpful.
|
Beta Was this translation helpful? Give feedback.
-
Thank you for this explanation @j4w8n. The team is actively working on new features within auth-js that makes it easier to verify the JWT (access token) soon. This should alleviate some of the problems with latency if you rely on
A leaked JWT secret opens up not only your users under attack (as they can be easily impersonated) but more severely your database (the secret can be used to mint your own JWTs with any role that can bypass RLS). |
Beta Was this translation helpful? Give feedback.
-
@j4w8n I have a simple question on this.
Is this only an issue though if there are "trusted" operations performed server side or server component side? If one performs queries for example in a server component and implements @supabase/ssr the mal formed attacker cookie would be passed to the server side supabase instance for example in a middleware in next.js or passed to an edgefunction that generate a server based client with it. For the middle ware In the old version in of the supabase docs (from a couple weeks ago) this still used If we stick to the old version
in an edge function same by reading the auth header from the request and passing that into the create client
this would use the mal formed cookie now and not verify it. But if one now performs a database query with this mal formed supabase client the query should fail on the backend as the backend would try to validate the malformed jwt that is send with the request? |
Beta Was this translation helpful? Give feedback.
-
Can I ask, other than |
Beta Was this translation helpful? Give feedback.
-
I am trying to build my app with Next.js and Supabase Auth following this doc: https://supabase.com/docs/guides/auth/server-side/nextjs Now the doc says we should use But The Next.js middleware runs on edge runtime, which is close to end users but may be farway from Supabase Auth server Then, the In my case, the latency is more than 500ms,from HongKong to the US.
I hope you guys can solve this problem as soon as possible, may be you can issue a signed idToken and read user profile from it. |
Beta Was this translation helpful? Give feedback.
-
As briefly described on PR supabase/auth-helpers#722, an attacker can retrieve sensitive user data through an attack vector within auth-js and a developer's code. This attack is based on a developer storing user sessions in cookies and using server-side rendering. In this discussion, I'd like to clarify the topic, from my perspective, and point out some solutions and pitfalls. I welcome your thoughts.
To summarize the above:
getSession()
on the server and then use unique data likesession.user.id
, to return that user's sensitive information.getSession()
to retrieve a user's tokens (access, refresh).getSession()
is likely safe, but be cautious - e.g. a public url for their avatar, viasession.user.user_metadata.avatar_url
in the case of a GitHub OAuth login. Just keep in mind that accessingsession.user
currently throws a console warning.getSession() vulernability
If we look at how this method works, we see that it runs minimal checks on the potential session it grabs from storage. When I say "minimal checks", I'm not casting any blame; Supabase can't do much more than what it already is.
null
.object
, and that it has the propertiesaccess_token
,refresh_token
, andexpires_at
.expires_at
is some point in the future, it returns the session to you.This seems reasonable, but if we craft a cookie with the following values, it would pass all of those checks:
Developer code
Say you're server-side rendering a user's page that's been requested. You check that the user is logged-in by calling
getSession()
- perhaps in middleware. Data from that call comes back to you, so it seems we have an authenticated user for this request. So, now you fetch the user's sensitive data based on the value ofsession.user.id
, then the page contents are returned and rendered in the browser. Everything is working great, but then...Attack!
For this attack to work, we need to assume the attacker was able to acquire a victim-user's Supabase user id. It also requires knowledge of the cookie name for user sessions, but this is trivial to find. We also assume the attacker does not have the user's access token or refresh token - otherwise they could just use those to get the victim's data.
Now imagine the attacker sends a page request to the server, using a properly-named cookie with this data:
Oh snap! All of the
getSession()
checks have passed and you've now sent the victim-user's sensitive data back to the attacker.Mitigation
!!!!!
Only use your JWT secret on the server-side, and only via a private env var. If there are other secure methods, I don't know what they are.
!!!!!
Here are a few things we can do to prevent this attack.
session
to render sensitive data. The only exception is verifying and decoding theaccess_token
, with a library such as jsonwebtoken, and then use information within the decoded JWT to load sensitive user data. So, from our example, instead of usingsession.user.id
, grab the value of thesub
property from a verified and decoded JWT - which is the user id of whoever the JWT was created for.getUser()
to retrieve the user's data and use it to fetch further data for the page. Yes, the getUser call grabs theaccess_token
from getSession and verifies it, but it's worth mentioning that you still can't trust anything else from thesession
info returned bygetSession()
. Anyway, if step 1 is not feasible or desired, calling getUser is the easiest solution. The downside is that it requires an extra network request every time you call it - either to Supabase's cloud instance or your self-hosted Supabase instance.False Security
When I came across this attack vector, my natural solution was to simply call
getSession()
and verify the JWT, akasession.access_token
. I assumed that as long as the JWT checks out, I can use data withinsession.user
to render the page.However, after thinking about it more, I realized this is a mistake. To understand why, imagine an attacker signs up for an actual account with your app and signs-in. They can then take their legit access token and send a separate request with the below cookie data:
As you can see, this would pass the
getSession()
checks and your JWT verification (access_token), but would still return the victim-user's data. So don't assume everything inside ofsession
can be trusted just because you were able to verify the access_token.If, instead, we were to grab the legit attacker's token and use the decoded JWT info from it, this would render the attacker's data instead of the would-be victim's. Just take precautions to only use your project's JWT secret, to verify/decode, on the server-side.
Beta Was this translation helpful? Give feedback.
All reactions