Skip to content
Back to field notes
FrameworksJune 6, 20266 min read

SvelteKit load functions and public data exposure before launch

A SvelteKit server load function that returns a whole database row ships every column to the browser in the page payload, even the ones your UI never renders. This note shows how to check what your load functions actually expose before you share the URL.

SvelteKitData exposureFrameworks

What a load function returns is what the browser receives

The concrete launch risk in a SvelteKit app is this: whatever a server load function returns is serialized into the page payload and delivered to the browser, including fields your UI never displays. A SvelteKit load function security gap is not about what you render. It is about what you return.

When a server load function in a +page.server.js or +layout.server.js file returns an object, SvelteKit serializes that whole object and sends it to the client. It lands in the initial HTML and, on later client-side navigations, in a separate data fetch the framework makes (the route's __data.json). So if a load function pulls a user row from the database and returns it as-is, every column on that row travels to the browser, whether or not a single one is shown on screen.

This is observable, not theoretical. You can open the page source or the network tab and read the serialized data directly. It does not require an exploit, a logged-in attacker, or a malformed request. A field is exposed the moment a load function returns it.

  • A returned row includes columns the page never displays: password_hash, stripe_customer_id, internal flags, soft-delete markers, other users' identifiers joined in.
  • The risk is over-exposure of data fields, a CWE-200 style information exposure, not a broken login.
  • This is separate from authorization. A load function can correctly require a session and still return far more about that user than the page needs.

Why AI-built SvelteKit apps hit this

The fast path produces an over-broad return shape. When you prompt an AI tool for "a profile page that shows the user's name and avatar," the generated load function typically does the simplest query that works: select the user row, return it. The page reads data.user.name and data.user.avatar, the screen looks right, and the task appears done.

What did not happen is the step where someone narrows the query or the return object to only the fields the page actually uses. Returning the full row is less code and renders identically, so there is no visible signal that anything is wrong. The exposed columns are invisible from inside the running app because nothing renders them.

  • A select * or a full ORM model return is shorter to write than an explicit field list, so the generated code favors it.
  • Universal load functions in +page.js run in the browser too, so any "secret" fetched there was never server-only to begin with.
  • Fast deploy means the public URL exists before anyone inspects the serialized payload, so the first audit is often the live site.

Check what your own pages actually serialize

You can confirm this on your own app with no attack and no special tools. Do it from a normal browser, logged in as a test user you own.

View the source of a server-rendered page and search it. SvelteKit embeds the serialized load data inline in the HTML. Look for the data your load functions returned and scan it for fields the page does not show. Then open DevTools, go to the Network tab, and trigger a client-side navigation to the route. SvelteKit fetches the route data as a separate request ending in __data.json. Open that response and read every field it contains.

A second pass: grep your own codebase for load functions that return a database query result or an ORM object directly, rather than an explicit object of named fields. Those are the return shapes most likely to over-expose.

  • Suspicious result: the payload contains columns the UI never renders, especially password hashes, tokens, billing identifiers, email addresses of other users, or internal-only flags.
  • Safer result: the payload contains only the fields the page reads and displays, and nothing that identifies or describes other users or internal state.
  • Repeat per route. Each +page.server.js and +layout.server.js has its own return shape, so one clean page tells you nothing about the next. Layout load data is especially worth checking, because it ships to every child route.

Trim the return shape, and know what that does not cover

The fix is to return only the fields the page needs, and to do it at the boundary where data leaves the server.

Select explicit columns in the query, or map the row to a small object before you return it from the load function. If a page shows a name and an avatar, return an object with exactly name and avatar, not the row. Do the same for lists: map each item to the fields the UI consumes. Keep any server-only value, such as an API key or a service token, out of the returned object entirely; if it needs to reach the browser at all, that is a sign to rethink the design rather than to widen the payload.

What this does not cover: trimming the payload is data minimization, not access control. It does not decide whether this caller is allowed to see this record at all. A load function that returns a tightly scoped object can still serve it to the wrong user if the query is not filtered by the session's identity. Authorization, which row belongs to which caller, is a separate control that you enforce in the query and verify by manual review. Field trimming reduces what leaks when something is exposed; it does not by itself stop the wrong person from loading the page.

  • Return a named projection of fields, not the whole row or model instance.
  • Never return secrets from a load function; for the public-versus-server-only split across frameworks, see server-only vs public env vars explained.
  • Move any value that is purely for server use into the action or endpoint that uses it, so it never enters the load return.

Returning fewer fields limits over-exposure. It does not validate that the logged-in caller should have access to that record in the first place. Keep the session-scoped filter in the query and confirm it by reading the code, not by checking the rendered page.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check inspects the page payload from the public surface and flags over-exposed data fields. An over-broad load return shape is a public-surface signal: the serialized data is right there in the HTML and the route data response, so a scan can spot secret-shaped strings and obvious over-exposure without any account or code access. That is the same thing an outside visitor can read, surfaced with severity and fix direction so you catch it before sharing the URL.

What it does not do is read your load functions or your queries. It sees the serialized output, not the intent behind it, so it cannot tell whether a field that looks benign should still be private for this user, and it cannot confirm that every route filters by session identity. Authorization correctness, query scoping, and the full set of fields each role should see are manual-review work. For the related pattern where the same over-broad shapes leak through JSON API responses, see PII in API responses in AI-built apps.

Next step: open each server-rendered route, read its serialized payload in the page source and the route data response, and trim every load function down to a named list of the fields the page uses. Then run a focused Launch Check against the public URL before you share it, so an over-exposed field shows up in your scan and not in someone else's view source.

A clean Launch Check on the payload does not prove your data model is correctly scoped. The scan reads what is reachable in the public payload; it does not evaluate whether each load function returns only what its caller is entitled to. Treat a flagged field as a launch blocker, and treat a clean result as one signal, not a guarantee.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.