codediag

CodeDiag / Guides / SvelteKit

Does an auth check in +layout.server.ts protect SvelteKit form actions?

Updated November 2, 2026 · SvelteKit 2

No. A redirect in a layout's load doesn't stop a form action or a +server endpoint under that layout. SvelteKit runs the action first and the load functions after it, and +layout files have no effect on +server.js.

Check auth in the handle hook in src/hooks.server.ts, or at the top of every action and endpoint.

Why the layout check comes too late

When a form posts to ?/delete, SvelteKit handles the request in this order:

  1. The handle hook runs.
  2. The action runs and does its work: here, it deletes a user.
  3. The page's load functions run to render the result. The layout's redirect fires now, after the delete.

The SvelteKit docs describe both halves: load functions run after the action completes, and handle runs before the action is invoked.

Endpoints are worse off. The routing docs say +layout files have no effect on +server.js files; per-request logic belongs in handle. Even for page loads, SvelteKit runs load functions concurrently, and its notes on authentication recommend hooks for protecting a group of routes.

What it looks like

A common setup: handle loads the user, the admin layout redirects anonymous visitors, and the page has an action.

src/hooks.server.ts
export const handle: Handle = async ({ event, resolve }) => {
  event.locals.user = await getUser(event.cookies.get("session"));
  return resolve(event);
};
src/routes/admin/+layout.server.ts
export const load = async ({ locals }) => {
  if (!locals.user) redirect(303, "/login");
  return { user: locals.user };
};
src/routes/admin/users/+page.server.ts
export const actions = {
  delete: async ({ request }) => {
    const data = await request.formData();
    await db.user.delete({ where: { id: String(data.get("id")) } });
  },
};

A signed-out visitor who opens /admin/users is redirected to the login page. The action is not protected. Anyone can post to it:

terminal
curl -X POST "https://app.example.com/admin/users?/delete" \
  -H "Origin: https://app.example.com" \
  -d "id=42"

The user is deleted, and the response is the layout's redirect. SvelteKit's CSRF check compares the Origin header to stop other sites' forms in a browser. A script can send any Origin it likes.

Fix it

Reject anonymous requests in handle

Best when a whole section of the app needs a signed-in user. It covers pages, actions, and endpoints under the path in one place.

src/hooks.server.ts
export const handle: Handle = async ({ event, resolve }) => {
  event.locals.user = await getUser(event.cookies.get("session"));

  if (event.url.pathname.startsWith("/admin") && !event.locals.user) {
    return new Response("Unauthorized", { status: 401 });
  }

  return resolve(event);
};

If you protect a route group such as (app) rather than a path prefix, test event.route.id instead. Keep the layout redirect if you like the user experience; it just isn't the security boundary.

Or check inside each action and endpoint

src/routes/admin/users/+page.server.ts
export const actions = {
  delete: async ({ request, locals }) => {
    if (!locals.user) error(401, "Unauthorized");
    const data = await request.formData();
    await db.user.delete({ where: { id: String(data.get("id")) } });
  },
};

Check every route at once

CodeDiag lists the SvelteKit actions and endpoints that nothing protects. It reads hooks.server.ts (including sequence()), every action and +server handler, and the helpers they call. For the example above it says:

$ npx @sabahattink/codediag scan .

  ⚠ Form action ?/delete on /admin/users has no recognizable auth check; the auth check in src/routes/admin/+layout.server.ts load does not run for form actions [missing-sveltekit-auth]
    src/routes/admin/users/+page.server.ts:4
    → Check locals.user in the action and return fail(401) or redirect(), or reject anonymous requests to this route in the handle hook; load functions do not run before form actions

Either fix above clears the finding. CodeDiag runs locally, needs no account, and stays offline with --no-audit. It is static analysis: "no recognizable auth check" means it could not see one, so read the cited file before calling a route vulnerable.

Questions

Does a +page.server.ts load protect the same page's actions?

No. The action runs first and the page's load functions run after it completes, so a redirect in the page load also comes too late.

Does handle run for form actions and +server endpoints?

Yes. It runs for every request the server receives, before the action is invoked. That is why it is the place to reject anonymous requests for a group of routes.

Isn't SvelteKit's CSRF protection enough?

No. The origin check stops other sites from submitting forms in a victim's browser. A script that sends the request directly can set any Origin header, so the action still needs an auth check.

Can an auth check in +layout.ts protect anything?

No. Universal load functions also run in the browser, so they are never a security boundary.

Sources

Find every route anyone can call

One command for SvelteKit, Next.js, React Router, Express, Fastify, Hono, Elysia, NestJS, and tRPC. Your coding agent can run it too, through the MCP server or the Claude Code plugin.

$ npx @sabahattink/codediag scan .

Related guides