SvelteKit Security Testing: VITE_ Env Exposure, SSR State Pollution, Endpoint Authentication

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.

Table of Contents

  1. VITE_ Environment Variable Exposure in Client Bundle
  2. SSR State Pollution and Load Function Data Leakage
  3. +server.ts Endpoint Authentication and Authorization
  4. CSRF Protection and Form Action Security
  5. SvelteKit Security Hardening

VITE_ Environment Variable Exposure in Client Bundle

# 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

SSR State Pollution and Load Function Data Leakage

# 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']*>\s*window\.__sveltekit_data[^<]+', html)
for m in matches:
    print(m[:400])
" 2>/dev/null

+server.ts Endpoint Authentication and Authorization

# SvelteKit +server.ts endpoint authentication — unauthenticated API routes
TARGET="https://your-sveltekit-app.example.com"

# SvelteKit +server.ts files create API endpoints with no authentication by default.
# A route at src/routes/api/users/+server.ts creates GET /api/users.
# If the handler doesn't check locals.user, it's publicly accessible.

# Step 1: Enumerate SvelteKit API endpoints by pattern
# SvelteKit routes follow the filesystem structure
ENDPOINTS=(
  "/api/users"
  "/api/user"
  "/api/users/me"
  "/api/profile"
  "/api/settings"
  "/api/admin"
  "/api/admin/users"
  "/api/data"
  "/api/export"
  "/api/webhooks"
  "/api/internal"
  "/api/debug"
  "/api/health"
  "/api/metrics"
  "/api/config"
)

for ENDPOINT in "${ENDPOINTS[@]}"; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${TARGET}${ENDPOINT}" 2>/dev/null)
  if [[ "$STATUS" =~ ^(200|201|204)$ ]]; then
    echo "[OPEN] ${ENDPOINT}: HTTP ${STATUS}"
    BODY=$(curl -s "${TARGET}${ENDPOINT}" 2>/dev/null | head -c 500)
    echo "  Response: ${BODY}"
  fi
done

# Step 2: Test HTTP method enumeration on found endpoints
# SvelteKit +server.ts explicitly exports GET, POST, PUT, DELETE handlers
for ENDPOINT in "/api/users" "/api/data" "/api/admin"; do
  for METHOD in "GET" "POST" "PUT" "PATCH" "DELETE"; do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
      -X "${METHOD}" "${TARGET}${ENDPOINT}" \
      -H "Content-Type: application/json" \
      -d '{}' 2>/dev/null)
    echo "${METHOD} ${ENDPOINT}: ${STATUS}"
  done
done

# Step 3: Check SvelteKit hooks.server.ts for authentication bypass
# The handle hook in hooks.server.ts runs before all requests
# A missing auth check in handle() leaves all routes unprotected

# Vulnerable hooks.server.ts pattern (no auth enforcement):
# export async function handle({ event, resolve }) {
#   event.locals.user = null;  // Auth never checked
#   return resolve(event);
# }

# Test: Access protected routes without session cookie
curl -s "${TARGET}/api/admin/users" 2>/dev/null | \
  python3 -c "import sys,json; d=json.load(sys.stdin); print(str(d)[:300])" 2>/dev/null

# Step 4: Test authorization — horizontal privilege escalation
# Can user A access user B's data via the API?
SESSION_A="your-session-token-A"
# Access own profile
curl -s "${TARGET}/api/users/me" \
  -H "Cookie: session=${SESSION_A}" 2>/dev/null | head -c 200

# Access another user's profile by ID
for USER_ID in 1 2 3 4 5; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    "${TARGET}/api/users/${USER_ID}" \
    -H "Cookie: session=${SESSION_A}" 2>/dev/null)
  echo "GET /api/users/${USER_ID} as user A: ${STATUS}"
done

# Step 5: Check for SvelteKit static files with sensitive data
# Static files in /static/ are served without any auth processing
for FILE in "/.env" "/.env.local" "/config.json" "/users.json" "/data.json"; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${TARGET}${FILE}" 2>/dev/null)
  echo "Static ${FILE}: HTTP ${STATUS}"
done

CSRF Protection and Form Action Security

# SvelteKit CSRF protection and form action security testing
TARGET="https://your-sveltekit-app.example.com"

# SvelteKit has built-in CSRF protection via checkOrigin in svelte.config.js
# Default: enabled — blocks form actions where Origin != Host
# Misconfiguration: checkOrigin: false disables protection for ALL form actions
#
# svelte.config.js (vulnerable):
# kit: { csrf: { checkOrigin: false } }  // Never do this

# Step 1: Test CSRF protection on SvelteKit form actions
# SvelteKit form actions are POST requests to ?/actionName
# Without checkOrigin, cross-site POST is possible

SESSION="your-authenticated-session-cookie"

# Test: Submit a form action from a different Origin
curl -s -X POST "${TARGET}/settings?/update" \
  -H "Cookie: session=${SESSION}" \
  -H "Origin: https://evil.example.com" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=attacker@evil.com" \
  -w "\nHTTP Status: %{http_code}\n" 2>/dev/null

# If HTTP 200 is returned, CSRF protection is disabled or bypassable

# Step 2: Test SvelteKit form actions by name
# SvelteKit routes can have multiple named actions: ?/create, ?/delete, ?/update
for ACTION in "create" "update" "delete" "submit" "save" "upload" "invite" "reset"; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST "${TARGET}/dashboard?/${ACTION}" \
    -H "Cookie: session=${SESSION}" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "test=1" 2>/dev/null)
  echo "POST /dashboard?/${ACTION}: ${STATUS}"
done

# Step 3: Bypass CSRF via Content-Type — application/json vs form-encoded
# SvelteKit's checkOrigin only applies to form submissions (application/x-www-form-urlencoded)
# If the server action also handles application/json, Origin check may be skipped
curl -s -X POST "${TARGET}/api/settings" \
  -H "Cookie: session=${SESSION}" \
  -H "Origin: https://evil.example.com" \
  -H "Content-Type: application/json" \
  -d '{"email":"attacker@evil.com"}' \
  -w "\nHTTP Status: %{http_code}\n" 2>/dev/null

# Step 4: Check for CSRF tokens in enhanced forms
# SvelteKit's enhance() adds client-side progressive enhancement but no extra CSRF token
# Test if the form action validates any custom CSRF token
curl -s -X POST "${TARGET}/account?/delete" \
  -H "Cookie: session=${SESSION}" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "confirm=true" \
  -w "\nHTTP Status: %{http_code}\n" 2>/dev/null

# Step 5: Test server-side redirection open redirect in form action response
# SvelteKit actions can return redirect() — test for open redirect
curl -s -X POST "${TARGET}/login?/login" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "redirect=https://evil.example.com&email=test@test.com&password=wrong" \
  -D - 2>/dev/null | grep -i "location:"

SvelteKit Security Hardening

SvelteKit Security Hardening Checklist:
Security TestMethodRisk
VITE_ env var exposure in client bundleDownload /_app/immutable/chunks/*.js — grep for API keys, secrets, DB URLs inlined by Vite build processCritical
SSR module-level state pollution between usersMake two sequential requests as different users — check if first user's data appears in second user's responseHigh
Unauthenticated +server.ts API endpointsGET /api/* without session cookie — enumerate routes, check for user data or admin functions without authHigh
CSRF bypass via checkOrigin: falsePOST /route?/action with Origin: https://evil.com — if accepted, CSRF protection is disabledHigh
+page.server.ts __data.json exposureGET /route/__data.json — SvelteKit data endpoint may return serialized load() data without auth checkMedium

Automate SvelteKit Security Testing

Ironimo tests SvelteKit applications for VITE_ prefixed environment variable exposure in client JavaScript bundles, SSR module-level state pollution between user requests, unauthenticated +server.ts API endpoint enumeration and data extraction, CSRF protection bypass via misconfigured checkOrigin, +page.server.ts __data.json serialized data endpoint exposure, hooks.server.ts authentication gap discovery, and horizontal privilege escalation in user-scoped API routes.

Start free scan