SvelteKit's file-based routing and tight SSR/CSR boundary creates a distinct attack surface that differs from React and Vue frameworks. Core risks: any environment variable prefixed with VITE_ is bundled into the client JavaScript bundle and readable by anyone; SSR load functions that store data in module-level variables leak between user requests; +server.ts API endpoints have no authentication by default; and SvelteKit's built-in CSRF protection only covers non-GET requests with a matching Origin header — a misconfigured checkOrigin option disables it entirely. This guide covers systematic SvelteKit security assessment.
# SvelteKit — VITE_ env variable exposure in client JavaScript bundle
TARGET="https://your-sveltekit-app.example.com"
# Step 1: Fetch the main client bundle to search for exposed secrets
# SvelteKit bundles all VITE_* env variables into _app/immutable/chunks/
curl -s "${TARGET}" | \
grep -oE '/_app/immutable/[^"]+\.js' | \
sort -u | head -10
# Step 2: Download each chunk and search for exposed secrets
# VITE_ prefix variables are inlined literally into the bundle at build time
for CHUNK_URL in $(curl -s "${TARGET}" | grep -oE '/_app/immutable/[^"]+\.js' | sort -u); do
echo "=== Scanning ${CHUNK_URL} ==="
curl -s "${TARGET}${CHUNK_URL}" 2>/dev/null | \
grep -oE '"[A-Z_]{5,}[_A-Z0-9]*":"[^"]+"' | \
grep -v '"undefined"' | head -20
done
# Step 3: Targeted search for common secret patterns in bundle
curl -s "${TARGET}/_app/immutable/chunks/start.js" 2>/dev/null | \
python3 -c "
import sys, re
bundle = sys.stdin.read()
patterns = [
('API_KEY', r'(?:VITE_)?(?:API[_-]?KEY)[\"\\x27]?\s*[:=]\s*[\"\\x27]([a-zA-Z0-9_\-\.]{16,})[\"\\x27]'),
('SECRET', r'(?:VITE_)?(?:SECRET|TOKEN)[\"\\x27]?\s*[:=]\s*[\"\\x27]([a-zA-Z0-9_\-\.]{16,})[\"\\x27]'),
('DATABASE_URL', r'(?:VITE_)?DATABASE_URL[\"\\x27]?\s*[:=]\s*[\"\\x27]((?:postgres|mysql|mongodb)[^\"\\x27]{10,})[\"\\x27]'),
('STRIPE', r'(?:sk_|pk_)(?:live|test)_[a-zA-Z0-9]{20,}'),
('SENTRY_DSN', r'https://[a-f0-9]{32}@[a-z0-9.]+/[0-9]+'),
]
for name, pattern in patterns:
matches = re.findall(pattern, bundle, re.IGNORECASE)
if matches:
print(f'[FOUND] {name}: {matches[0][:80]}')
" 2>/dev/null
# Step 4: Check .env file patterns — distinguish safe vs unsafe
# SAFE (server-only): DATABASE_URL=postgres://... SECRET_KEY=...
# UNSAFE (client-exposed): VITE_DATABASE_URL=... VITE_SECRET_KEY=...
# The VITE_ prefix causes Vite to inline the value at build time
# Step 5: Check SvelteKit /api/ routes for unauthenticated data exposure
for ENDPOINT in "/api/user" "/api/users" "/api/config" "/api/settings" "/api/admin" "/api/health"; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${TARGET}${ENDPOINT}" 2>/dev/null)
if [ "$STATUS" = "200" ]; then
echo "[OPEN] ${ENDPOINT}: HTTP ${STATUS}"
curl -s "${TARGET}${ENDPOINT}" 2>/dev/null | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print(str(d)[:200])
except: print('(non-JSON response)')
"
fi
done
# SvelteKit SSR state pollution — shared module state leaks between user requests
TARGET="https://your-sveltekit-app.example.com"
# SvelteKit SSR runs in a persistent Node.js process.
# Module-level variables are shared across all requests.
# A vulnerable pattern:
#
# src/routes/dashboard/+page.server.ts:
# let cachedUser = null; // BUG: module-level, shared across all requests
# export async function load({ locals }) {
# if (!cachedUser) cachedUser = await getUser(locals.userId);
# return { user: cachedUser }; // First user's data served to all subsequent users
# }
#
# Safe pattern uses request-scoped locals, not module-level variables.
# Test for SSR state pollution — make two requests as different users
# and check if user A's data appears in user B's response
# Step 1: Login as user A and capture a unique session token
SESSION_A=$(curl -s -c /tmp/sk_cookies_a.txt -b /tmp/sk_cookies_a.txt \
-X POST "${TARGET}/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=userA@example.com&password=passwordA" \
-D - 2>/dev/null | grep -i "set-cookie" | head -1)
echo "Session A: ${SESSION_A}"
# Step 2: Make a request as user A to prime any cached state
curl -s -c /tmp/sk_cookies_a.txt -b /tmp/sk_cookies_a.txt \
"${TARGET}/dashboard" 2>/dev/null | grep -i "userA\|email\|profile" | head -5
# Step 3: Immediately request the same page without credentials
# If SSR caches module-level state, user A's data may leak
curl -s "${TARGET}/dashboard" 2>/dev/null | \
grep -i "userA\|email\|profile" | head -5
# Step 4: Test SvelteKit page data endpoint directly
# SvelteKit exposes __data.json endpoints for each route
for ROUTE in "/" "/dashboard" "/profile" "/admin" "/settings"; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
"${TARGET}${ROUTE}__data.json" 2>/dev/null)
if [ "$STATUS" = "200" ]; then
echo "[DATA ENDPOINT] ${ROUTE}__data.json: HTTP ${STATUS}"
curl -s "${TARGET}${ROUTE}__data.json" 2>/dev/null | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(str(d)[:300])" 2>/dev/null
fi
done
# Step 5: Check if sensitive load() data is embedded in page HTML
# SvelteKit inlines serialized load() data in a ' | head -3
curl -s "${TARGET}/dashboard" 2>/dev/null | \
python3 -c "
import sys, re
html = sys.stdin.read()
# SvelteKit serializes data in sveltekit:data script tags
matches = re.findall(r'