Repository navigation
Remote functions are unnecessarily dangerous to use #17090
Description
Activity
We do mention that this exposes an HTTP endpoint in https://svelte.dev/docs/kit/remote-functions#query-Query-arguments and mention that this is an attack vector in https://svelte.dev/docs/kit/remote-functions#Handling-validation-errors
As for formulating another 'guard' layer as separate from the existing 'validation' error, that doesn't feel like the right move to me.
I don't know what exposing a subset of the
RequestEventas opposed to the whole thing would achieve.@Conduitry yes it's mentioned, and I might just be stupid but I didn't really realize what it meant for a very long time. If you search for "auth" in the remote-functions docs, you only see one actual example of it being used, and little mention of how important it is. I originally copied the getPosts example and I ended up making a vulnerability in my app where users could just get any post through that endpoint, even though not all posts were meant to be public. I had some pages gated, but the remote function i used to pull the posts was not gated and it took me a long time to realize it.
It was discussed in this blog I found https://matt.dekok.app/blog/sveltekit-remote-functions#guarded-remote-functions and I think it's at the very least a really important example to give to the user. I don't think this should be left as an exercise to the reader, and I think it would be more svelte-like to suggest it in the function parameters that this might need a guard, and to make it visible and verifiable with tooling. As I said in the post, the security boundary is mixed into the handler body and relies on every author remembering to add it - it's an unnecessarily dangerous pattern imo. I think the decision to remove access to
event.urletc from inside a remote function is proof of that - people kept using it just like I did.I'll just delete the stuff about RequestEvent, you're right it doesn't really matter.
elliott-with-the-longest-name-on-github commented
on Sep 10, 2026 ContributorMore actionsHonestly, I'm currently still in the camp that this is primarily a documentation problem as opposed to an API problem. Any
guard-related API I can think of is either a), very complicated, in which case you're probably better just implementing your own version, or b) not complicated enough, where you'd commonly run into cases where you need to break out of the guard and end up running authorization logic inside the function body anyway.Fundamentally remote functions are no different from api endpoints in that, yeah, if you want them to be protected by authorization, you have to, well, protect them by authorization.
The part of this that resonates with me the most is the "assert all functions are guarded" functionality, but I honestly don't know that there's a great one-size-fits-all solution to that. The correct solution is for your application to have integration tests that assert that remote functions you expose are only accessible to the roles you intend to expose them to. A blanked "all must be guarded" policy is just creating false security, as it's trivial to accidentally create a guard that does nothing, leaves out important cases, etc.
Hi elliott, love your work and your unforgettable username!
Honestly, I'm currently still in the camp that this is primarily a documentation problem as opposed to an API problem. Any
guard-related API I can think of is either a), very complicated, in which case you're probably better just implementing your own version, or b) not complicated enough, where you'd commonly run into cases where you need to break out of the guard and end up running authorization logic inside the function body anyway.Yeah it's definitely a documentation problem, as there is currently no direction on how to actually do auth properly when dealing with remote functions. I think it's not really up to sveltekit to decide if the guard is complicated enough, but having the feature would enable users to develop solutions. I think a reasonable guard would be a check if there is a user, like in the example from the docs:
const user = await auth.getUser(); if (!user) error(401, 'Unauthorized');
I feel like this is so common that it should be reusable? Or am I really supposed to include it in every single remote function?
Fundamentally remote functions are no different from api endpoints in that, yeah, if you want them to be protected by authorization, you have to, well, protect them by authorization.
Yes but the trick is that you can't do it based on routes in
hooks.server.tslike you can with other routes. It's not intuitive to the user.The part of this that resonates with me the most is the "assert all functions are guarded" functionality, but I honestly don't know that there's a great one-size-fits-all solution to that. The correct solution is for your application to have integration tests that assert that remote functions you expose are only accessible to the roles you intend to expose them to. A blanked "all must be guarded" policy is just creating false security, as it's trivial to accidentally create a guard that does nothing, leaves out important cases, etc.
Happy to hear that some of it resonates with you! I have to go to bed, I'll see if there are more responses tomorrow. I'm still not convinced I'm wrong!
elliott-with-the-longest-name-on-github commented
on Sep 11, 2026 ContributorMore actionsThe problem is, this:
export const getSecretThing = query( () => { // data-getting stuff }, () => { const user = await auth.getUser(); if (!user) error(401, 'Unauthorized'); } )
Is just worse in every way than this:
export const getSecretThing = query( () => { const user = await auth.getUser(); if (!user) error(401, 'Unauthorized'); // data-getting stuff } )
- The thing that comes second runs first
- What if you need the user in the main body of the function, for example to look up a database record by user ID? You just have to call
await auth.getUser()again? - It's less composable
- It means the third argument is now taken, so if we ever want to be able to pass any options to
query, you'd have to doquery(schema, fn, undefined, { ... })- The alternative is to make
guardpart of an options object, which is just even worse
- The alternative is to make
Yes but the trick is that you can't do it based on routes in hooks.server.ts like you can with other routes. It's not intuitive to the user.
Guarding based on route is just always bad. You shouldn't do it unless it's just to increase UX by saying "hey, you're not allowed to be here". Your access controls are about what data is being accessed, not where it's being accessed from. This is the same everywhere.
For what it's worth, it's actually not very hard to write an ESLint plugin that enforces authorization for all queries. Example:
import { query } from '$app/server'; import { auth, Role, Scope } from '$lib/auth'; export const getSecretThing = query( () => { const user = await auth.getUser(); await auth.can(user, Role.Read, Scope.Team); // data-getting stuff } )
If this is what your auth looks like, it's pretty simple to throw together a plugin that:
- Looks for calls to
query,command, orformimported from$app/server - Inspects their function body and makes sure the body calls and awaits
auth.can
Your AI can throw that together in probably about 45 seconds and it's way more flexible than anything we could come up with.
Reacted by Samuel Plumppu and Dmitrii Gorbunov- Reacted by simonfelding, Enrico Sacchetti and Samuel Plumppu
The problem is, this:
[...]
Absolutely, a silly mistake.
- The thing that comes second runs first
So the way I currently do it is with a wrapper like
guardedQuery(guard, ...)exactly for that readability and order of execution. I was just trying to propose something that wouldn't be a breaking change, but I do prefer the guard in front as well. I'm sure you're right about the other criticisms.Guarding based on route is just always bad. You shouldn't do it unless it's just to increase UX by saying "hey, you're not allowed to be here". Your access controls are about what data is being accessed, not where it's being accessed from. This is the same everywhere.
I was not aware of this. From my point of view, it feels very natural in sveltekit to guard based on the file structure (just like
src/lib/serveris not accessible from client side). I am a happy user of a pattern like this:// hooks.server.ts export const handle: Handle = async ({ event, resolve }) => { const isProtectedRoute = event.route.id.startsWith('/(protected)'); if (event.locals.userId === 'guest' && isProtectedRoute) { throw redirect(303, `/login`); }
I didn't know it was a bad idea. It works perfectly fine with load functions, and seems to me like a logical way to seperate concerns, and it looks pretty clean to me. It was the first thing that came to mind when I read that I can group my routes like this. I really like the idea that I can tell what routes are public and what routes are private, based on their path. It makes it easy for me to verify if a route is protected or not. It's also recommended in the docs, so I'd say it's not unreasonable that I came to think that remote functions would respect this pattern:
There are a few possible strategies to ensure an auth check occurs before protected code.
To prevent data waterfalls and preserve layout load caches:- Use hooks to protect multiple routes before any load functions run
But it of course doesn't work that way with remote functions, and it's not intuitive from the way they are described in the docs how dangerous this pattern is when paired with remote functions (just to reiterate to the reader - because they expose a public endpoint at
/_app/no matter where you call them from).For what it's worth, it's actually not very hard to write an ESLint plugin that enforces authorization for all queries. Example:
Absolutely, and if that's the recommended pattern I can just do that - but it requires awareness of the issue before that thought comes to mind. I'm pretty sure AI will happily write this kind of code too, so there's definitely a documentation issue as you mentioned earlier.
My point is that problem is that it breaks with what I consider to be svelte-like, and I really like the safeguards SvelteKit has in other places to prevent doofuses like me from including server functions in the client side for example. From a doofus user point of view, remote functions break the expectation that sveltekit protects me from exposing server functions to the user, they break with the (apparently bad) path based routing + hooks pattern, and with average reading comprehension and attention span it sounds like the docs say they provide a safe way to expose server functions to the client. They are safe - if you know about the dangerous footgun and set up ESLint to protect you. Following this logic you propose, we could also do away with blocking the client from running code from
/src/lib/server, as path-based guards are always bad and an AI-generated ESLint plugin could easily defend against this obvious mistake.Finally, regarding:
Your AI can throw that together in probably about 45 seconds and it's way more flexible than anything we could come up with.
A good point, but also a counter-argument to what you said earlier about how the complexity of writing a good guard is beyond the average user. I don't think the complexity of a good RBAC solution is as much of a barrier as it used to be, and what we need nowadays is more of the linter/framework-level safety features, such as (an optional) strict requirement for remote function guards.
- addedneeds-decisionNot sure if we want to do this yet, also design work neededNot sure if we want to do this yet, also design work needed
on Sep 13, 2026 I personally had to add the following line in hyunbinseo/svelte-kit-template@e105dcd since Claude kept skipping auth checks.
Since (Remote Functions) they generate HTTP endpoints, the request must be appropriately authenticated and authorized.
It's fixed afterwords so I've resolved it with docs + agent reviews.
This is the current form:
Remote Functions (RPC)
- Remote functions must be exported from
*.remote.tsfiles. - There are 4 types:
command,form,query,prerender. - Requests must be either public, or authenticated and authorized.
- Inside callbacks,
event.urlrefers to the page, not the endpoint.
import { form, query } from '$app/server'; import { requireLoggedOut, requireSession } from '#lib/server/auth/session.ts'; export const getPublicPosts = query(async () => { // Use prerender if static or cacheable. }); export const getPrivatePosts = query(async () => { const session = requireSession(); }); export const sendLoginCode = form(PublicSendCodeSchema, async (data, issue) => { requireLoggedOut(); // must be logged out });
Reacted by Enrico Sacchetti- Remote functions must be exported from
Describe the problem
Remote functions are convenient because they make server operations look and feel like normal functions, but they are still public HTTP endpoints. This makes authorization a particularly easy concern to omit or implement inconsistently, leaidng to issues like #16452 #16416 #16815
The reason is mostly a consequence of remote functions being RPC endpoints inside a client-routed application. Sveltekit has a reliable path-based routing concept, but remote functions have a fundamentally different security model from normal page navigation.
Describe the proposed solution
First of all, I think the docs should make it clear that remote functions open endpoints that listen on _app/, because it's all too easy to think that they relate to the page they are called from - this is how everything else in sveltekit works after all.
I would like to propose adding an optional final
guardparameter to remote functions, together with an opt-in strict mode that requires guards for relevant remote functions. This has already been discussed a year ago, but I think the conclusion that it should be handled inside the function is wrong.Originally posted by @elliott-with-the-longest-name-on-github in #13897 (comment)
And yes it's true, but the security boundary is mixed into the handler body and relies on every author remembering to add it. It feels very un-svelte like to force the user to remember critical boiler-plate with no indication that it might be dangerous to omit it. And this solution makes it impossible for tooling to verify that the remote function doesn't produce gaping security holes, which I find they are very prone to - especially because you need to scan the build output to realize that you are exposing everything in your app and letting people use admin controls via a completely invisible /_app/ route. tbh I think it's not fair to the user to make them learn to rely on the file-based routing and then introduce this without any safeguards.
Proposed API
Just a optional final parameter to the relevant functions.
For overloads without a schema, the guard would simply remain the final argument:
The important property is that the existing argument order is preserved. The guard is an additional trailing concern rather than a new wrapper API or a reordering of the existing remote-function API.
Why a dedicated guard?
Today the equivalent commonly looks something like:
That works, but the security boundary is mixed into the handler body and relies on every author remembering to add it. It feels very un-svelte like to force the user to remember critical boiler-plate with no indication that it might be dangerous to omit it.
A guard makes the authorization boundary visible at the declaration site:
It also gives tooling a structural way to determine whether a remote operation has an explicit authorization policy.
Strict mode
I think the guard should remain optional at the API level so that SvelteKit does not impose an authorization model on every application.
However, projects should be able to opt into requiring it. I could really use this, and I think it would make LLM-based web-development safer too, as I find they frequently mess this up and produce dangerous remote functions. Humans probably do too.
For example, via Svelte config or an ESLint rule:
or equivalently through an official lint configuration.
There could also be an explicit way to say that a function intentionally has no additional authorization condition, rather than encouraging
() => true.For example:
This would make exceptions obvious during code review.
Why make strictness tooling-driven?
Different applications have different security requirements.
A documentation site may reasonably use remote queries without authorization. A case-management, banking, healthcare, enterprise, or multi-tenant application may want every remote operation to have an explicit authorization decision.
Keeping the third argument optional while allowing projects to require it gives both use cases a clean API and avoids changing the default behavior of existing applications.
Avoid using route context as authorization context
Another reason I think a first-class guard would be useful is that authorization for remote functions is based on:
localsand remote functions should indicate to users that they are not safe to use without a guard, and that the guard cannot be based on route-related request context because the client can manipulate it.
Authentication vs authorization
I think it's worth keeping authentication and the guard concept separate.
For example, sveltekit applications commonly establish user identity in
hooks.server.tsand place it inlocals.The guard then answers the operation-specific question:
or the resource-specific question:
This keeps SvelteKit neutral about how authentication and permissions are implemented.
Tooling opportunity
A first-class guard parameter would also allow much better tooling than is possible when authorization is arbitrary code somewhere inside the handler.
An ESLint rule could reliably flag:
when
requireGuardsis enabled.It could potentially also warn about questionable guard usage in the future.
Alternatives considered
I've been struggling with other ways to do it lately because I keep making unsafe remote functions, and until today I had settled on wrappers such as:
(I later found out this was already proposed by @sillvva who suggests this exact pattern: https://matt.dekok.app/blog/sveltekit-remote-functions)
Which is nice because it's clear from the declaration if the query or command is guarded or not, but a native optional final argument would preserve the standard API and give frameworks/libraries/tooling a stable extension point.
I also thought about extending sveltekit to use a HMAC-based system to produce cryptographically verifiable route origins that the server can trust, but it would add a lot of complex machinery and I doubt that the SvelteKit authors would like that in their beautiful project.
Importance
would make my life easier
Additional Information
I am planning to open a PR with an implementation sketch to explore the API and typing details.
The main questions I would like feedback on are:
allowmarker desirable for strict-mode exceptions?The goal is not to prescribe a particular authentication or RBAC system. It is to make authorization an explicit, composable part of a remote function declaration and to give security-sensitive applications a way to make omission detectable by tooling.