Help Me Understand "use server" Security
I'm new to this topic. So please excuse me if my questions if they're obvious or don't sound well thought out. I'm simply trying to learn.
Let's assume I have an app dashboard. When a user visits that dashboard, I run a createEffect() to check their access status.
Inside that "client-side"
createEffect()
I call a server function getUserRecord()
Here's a simplified example to show logic.
Here's the getUserRecord()
function:
For context, here's the getSupabaseAdminClient()
function:
31 Replies
Here's the question:
Technically, I'm calling the
getUserRecord()
function from the client... which leads me to believe that the getUserRecord()
function is exposed to the client.
Therefore, I'm wondering if it is possible for someone to repeatedly call getUserRecord()
from the client? Can they open Chrome dev tools or hack into something to gain access to getUserRecord()
and run it from the client with different variables?
This might not be a huge concern when I'm reading data, but this could quickly become an issue if exposed a server function that wrote data to my database.
Am I thinking about this correctly?
If so, how do I securely call server function from the client?Therefore, is it possible for someone to repeatedly call getUserRecord() from the clientYes, any
use server
function can be called from the client
Can they open Chrome dev tools or hack into something to gain access to getUserRecord() and run it from the client with different variables?Yes, they'll be able to supply their own
userId
value
If so, how do I securely call server function from the client?Get an auth token into the server function and authenticate the user in there. Whether you do that with cookies or by passing it as an argument is up to you
Ok... if I pass an auth token to my server function, couldn't any hacker just sign up for an account and pass that check?
For the case of
getUserRecord
i assume you'd want it to get the data of the current user, so use the auth token to figure out the user id to use instead of passing it as an argumentOk that makes sense.
What about this:
Take my original logic one step further:
Now I'm exposing
createUserRecord()
to the client.
If this function is exposed to the client, what prevents a hacker from continually writing to my DB?
Would I add a "unique" clause to my user table to prevent multiple rows with the same userId?You'd probably want to make emails unique and personally i'd generate user IDs on the server instead of client
Since signups are a public thing you'll probably want rate limiting at some point
And you'll probably want an email verification step at some point
userId should also be unique though yeah
Boy... apps get complicated the further you go, don't they?
You think you have something all figured out... then you start thinking and realize there's another hole somewhere.
I've got some homework to do.
Last question...
Let's say I'm using a
cache()
and load()
function from the router like this:
Is getOrders()
exposed to the client, even if it's driven by the solid router? I think it is, right?Yes since there's a
use server
there
Solid router has basically no knowledge of server functions, it'll just use what you give itSo a hacker get simply call
getOrders()
can get all of the orders?Yeah
So how do people get order securely?
Auth token
Can you point me in a direction? Where can I read about this?
I assume supabase would have some examples, but the basic premise is that you get the auth token into the function, whether with cookies or as an argument or otherwise, and then make sure the auth token belongs to someone who is an admin
Whether that's by fetching the user info from your database or validatin the auth token itself depends on the implementation
Ok... That's a great start.
Thank you for all of the help with this. I see a full day or two ahead of me.
Have a great night.
Fwiw i think you're doing this the hard way - supabase can already manage database access for you with its auth
Using the admin sdk is way more effort since you have to secure everything yourself
You mean with Row Level Security?
Yeah
Ok... I'm definitley new to this... so I'm open to any suggestions.
I had a model like this working earlier. Maybe I'll revisit that logic tomorrow and see if I can improve my RLS policies.
The only tricky part is that you'd need to create the supabase client differently on the client and server
Since on the client it'll automatically have access to cookies and stuff but on the server you need to provide them yourself
That part I'm pretty comfortable with.
I ran into some issues with the policies earlier, so I thought, "just run it on the server... that's secure"... but then I kept thinking and realized that might not be as secure as I thought.
I'll take a fresh look at this tomorrow. If I can improve my policies, your approach will be better and easier.
Thanks again!
Ok. I've spent way too much time on this, but I learned a lot.
I could grant access using RLS policies, but keeping access logic on the server makes more sense to me. This pattern also appears to have benefits, and I think it is very secure unless I'm missing something.
Here's what I did.
1. Turn on RLS for all tables.
2. Don't create any policies. This locks client-side code from interacting with the DB altogether.
3. Use the server to control CRUD operations and access.
Here's how I did it.
Unlike Next, which is server first, SolidStart is client first. So instead of using the @supabase/ssr package to monitor Auth State, I used a regular Context Provider to monitor Auth State on the client. Supabase has an
onAuthStateChange()
function that monitors changes in Auth State.
onAuthStateChange()
provides an event
and session
when it detects changes in Auth state. I use that session
to
1. Update the provider (so the client has access to the session)
2. Set a cookie (so the server has access to the session)
---
Here's a simplified example:
Then I can easily access the user on the server like this:
I think this approach is beneficial for the following reasons.
1. It's immediately apparent what's happening when reading the code base. I don't have switch from server functions to RLS policies to figure out what's going on.
2. I don't have to worry about RLS policies changing. If an RLS policy changes, my app could stop working if the server or client code doesn't align with the new RLS policy.
3. At my current level of understanding, this seems very secure. The odds of a hacker guessing an access token are next to impossible.
4. I can use rate limiting and unique constraints to handle most other concerns.
5. Admin roles are also pretty simple to manage. I can create a table of admin users and check if the current user is in the table. Again a hacker would have to guess an access token to get past this check, which is next to impossible.
Does this seem logical and secure?Is your app using ssr?
Yes, my app is using SSR
Not sure i agree with the characterisation that start is client-first but it's certainly a bit easier to do client stuff. Regardless, this seems like a fine approach
Good point.
I might not have the terminology correct.
I guess what I'm trying to say is in Next, you have to opt into "use client." By default, all components are server components.
But SolidStart doesn't have "server components." Everything is a "client component" that can call functions that live on the server.
yeah that's good enough, i could be more pedantic but it's not worth it haha
though start will be getting islands eventually
I don't mind pedantic, haha. I'm learning so, I've been trying to dig deep into this stuff and really understand how it all works.
well technically no components are server or client by default, their status as server or client depends on where they're imported from
entrypoints like
page.ts
, layout.ts
etc are server by default, but a random file without use client
will be isomorphic and can end up in either or both the client and server module graphs
back to your auth approach, it's secure as long as you correctly check that data you're accessing or mutating is owned by the current userYou said:
"as you correctly check that data you're accessing or mutating is owned by the current user"
I agree. I think that's pretty straightforward once I use the access token to get the current user.
Once I have the current user I can make sure the data I'm accessing or mutating is owned by the current user with a simple filter. Supabase uses
.eq()
yeah that'll do it
Thank you! I really appreciate all your help with this. I'll explore your points above a bit more.