security/shared-state-import · Shared runes-state import on the server
A Kit server/universal file imports a .svelte.ts module holding module-scope $state, one shared instance per server process.
Severity: warning · Category: security
What it checks
Flags an import in a SvelteKit route/hooks file whose specifier resolves to a repo-local .svelte.ts/.svelte.js module with module-scope $state (a top-level $state(...) declaration, or a module-scope instance of a class with $state fields). Two flavours:
- the server code mutates the imported state, outside handlers (
security/handler-state-writereports handler writes as critical), or - the import is read-only, and on the server the state is still one shared instance that keeps its boot-time value.
Direct imports only ($lib/… and relative specifiers); import type is excluded. Client-only usage of such modules, the idiomatic shared-store pattern, is fine and never flagged; only imports from server-executed files are.
Not flagged: a universal +page.ts/+layout.ts file that itself exports ssr = false. SvelteKit’s state-management docs: “If you’re not using SSR, then there’s no risk of accidentally exposing one user’s data to another.” That file’s load never runs on the server, so there’s no shared server instance to leak through. The exemption is same-file only, so a +page.server.ts (always server-executed, regardless of ssr) still gets flagged.
An extensionless ….svelte specifier canonicalises to ….svelte.ts for resolution purposes, so importing the component X.svelte while a $state-holding X.svelte.ts sibling exists can misattribute a finding to the component import, a rare naming coincidence.
Why it matters
On the browser each user gets their own module instance; on the server there is exactly one, shared by every request. If it ever holds per-user data, users see each other’s data; even if it doesn’t, server reads see a stale boot-time value rather than what the current user’s client sees.
How to fix
Don’t reach for shared module state in server-executed code. Return data from load and pass it via page.data or the context API:
import { quizState } from '$lib/quiz.svelte.js'; // ❌ one instance for all users on the server
export async function load({ locals }) {
return { bookmarks: await db.bookmarksFor(locals.user) }; // ✅
}
If the module is genuinely client-only, restructure so server files don’t import it. If the import is deliberate and safe, add // svelte-vitals-disable-next-line security/shared-state-import above it instead.
Mode differences
None. This rule reads source, the same .svelte and .ts files, everywhere it runs. The CLI, the Vite plugin’s build pass, and the live dashboard’s static baseline all report it identically, and the rendered-HTML pass never re-evaluates it. Scoping a run with --route skips it: component-scoped rules have no route to attribute a finding to.
Disabling
Silence a single occurrence with <!-- svelte-vitals-disable-next-line security/shared-state-import --> on the line above it, or turn the rule off:
export default {
rules: {
'security/shared-state-import': 'off'
}
};