| name | security |
| description | Security patterns for Velociraptor stack. Use when implementing authentication, handling user input, writing database queries, configuring headers, or deploying to Vercel. Covers SvelteKit CSRF, Better Auth gotchas, Drizzle/Neo4j injection, serverless security, and rate limiting. Essential for any security-sensitive code. |
Security
Defense-in-depth security patterns for the Velociraptor stack. Focuses on commonly misunderstood vulnerabilities and post-training knowledge gaps.
Contents
| Threat | Primary Defense | Secondary Defense |
|---|
| CSRF | SvelteKit origin check | Custom token for JSON APIs |
| SQL Injection | Drizzle parameterization | Never use sql.raw() with user input |
| XSS | Svelte auto-escaping | DOMPurify for {@html} |
| Session Hijacking | Better Auth httpOnly cookies | Short session expiry |
| DoS | Rate limiting | Connection pooling |
Critical Gotchas
These will break your security if you don't know them:
| Gotcha | What Goes Wrong | Fix |
|---|
| JSON API CSRF | Built-in protection only covers forms | Add custom CSRF header check |
| Better Auth rate limiting | Broken if IP headers spoofable | Configure ipAddressHeaders for Vercel |
sql.raw() in Drizzle | Direct SQL injection | Never use with user input |
| Neo4j dynamic labels | Cannot parameterize labels | Sanitize or restructure query |
{@html} in Svelte | XSS vulnerability | Use DOMPurify |
| Bun < 1.1.30 | Prototype pollution CVE | Upgrade immediately |
SvelteKit CSRF
Built-in Protection
SvelteKit validates origin headers automatically in production:
- Covers
POST, PUT, PATCH, DELETE
- Only for form-like content types
- Does NOT protect JSON APIs
export default {
kit: {
csrf: {
checkOrigin: true
}
}
};
JSON API Protection (Required)
import { error } from '@sveltejs/kit';
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
if (event.request.method !== 'GET' &&
event.request.headers.get('content-type')?.includes('application/json')) {
const csrfHeader = event.request.headers.get('x-csrf-token');
if (!csrfHeader) {
error(403, 'CSRF token required');
}
}
return resolve(event);
};
async function secureFetch(url: string, options: RequestInit = {}) {
return fetch(url, {
...options,
headers: {
...options.headers,
'x-csrf-token': '1',
},
});
}
Past CVE (Fixed)
CVE-2023-29003: CSRF bypass via uppercase Content-Type or text/plain. Fixed in SvelteKit 1.15.1+.
Server-Only Code
Three Protection Mechanisms
import { SECRET_KEY } from '$env/static/private';
export const API_KEY = process.env.SECRET_KEY;
export const db = drizzle(connection);
All three cause build errors if imported in client code.
Better Auth Security
Known Vulnerabilities (Monitor!)
| CVE | Severity | Description | Status |
|---|
| GHSA-99h5-pjcv-gr6v | High | Unauthenticated API Key Creation | Oct 2025 |
| GHSA-vp58-j275-797x | High | trustedOrigins Bypass → ATO | Feb 2025 |
| GHSA-hjpm-7mrm-26w8 | High | Open Redirect via Callback | Feb 2025 |
Action: Always use latest version. Monitor Better Auth Security Advisories.
Rate Limiting Configuration
export const auth = betterAuth({
advanced: {
ipAddressHeaders: ['x-forwarded-for'],
},
rateLimit: {
window: 60,
max: 10,
},
});
Warning: If users can spoof IP headers, rate limiting is completely bypassed.
Session Security
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 7,
updateAge: 60 * 60 * 24,
cookieCache: {
enabled: true,
maxAge: 60 * 5,
},
},
});
SQL Injection (Drizzle)
Safe Pattern (Parameterized)
import { sql } from 'drizzle-orm';
const userId = req.params.id;
const result = await db.execute(
sql`SELECT * FROM ${users} WHERE ${users.id} = ${userId}`
);
Query Builder (Always Safe)
await db.select()
.from(users)
.where(eq(users.id, userId));
DANGEROUS: sql.raw()
const result = await db.execute(
sql.raw(`SELECT * FROM users WHERE id = ${userId}`)
);
Only use sql.raw() for trusted, pre-constructed SQL strings.
Cypher Injection (Neo4j)
What Can Be Parameterized
| Element | Parameterizable | Example |
|---|
| Literals | Yes | $userId |
| Property values | Yes | SET n.name = $name |
| Labels | NO | :User cannot be $label |
| Relationship types | NO | -[:KNOWS]- cannot be $type |
Safe Pattern
const session = driver.session();
await session.run(
'MATCH (u:User {id: $userId}) RETURN u',
{ userId: userInput }
);
Dynamic Labels (Dangerous)
const label = req.params.type;
await session.run(`MATCH (n:${label}) RETURN n`);
await session.run(
'MATCH (n) WHERE n.type = $type RETURN n',
{ type: userInput }
);
If Dynamic Labels Unavoidable
function sanitizeIdentifier(input: string): string {
return input.replace(/[^a-zA-Z0-9_]/g, '');
}
const sanitized = sanitizeIdentifier(userInput);
await session.run(`MATCH (n:\`${sanitized}\`) RETURN n`);
Input Validation
Valibot for Structure
import * as v from 'valibot';
const UserInput = v.object({
email: v.pipe(v.string(), v.email()),
bio: v.pipe(v.string(), v.maxLength(500)),
});
XSS Prevention
<!-- SAFE - auto-escaped -->
<p>{userInput}</p>
<!-- DANGEROUS - unescaped HTML -->
<p>{@html userInput}</p>
If You Must Allow HTML
import DOMPurify from 'isomorphic-dompurify';
const sanitized = DOMPurify.sanitize(userInput);
<div>{@html sanitized}</div>
CSP Headers
export default {
kit: {
csp: {
mode: 'auto',
directives: {
'default-src': ['self'],
'script-src': ['self'],
'style-src': ['self', 'unsafe-inline'],
'img-src': ['self', 'data:', 'https:'],
'connect-src': ['self'],
},
},
},
};
Using Nonces
<script nonce="%sveltekit.nonce%">
</script>
Rate Limiting
Better Auth rate limiting is unreliable. Use sveltekit-rate-limiter:
import { RateLimiter } from 'sveltekit-rate-limiter/server';
const limiter = new RateLimiter({
IP: [10, 'h'],
IPUA: [5, 'm'],
});
export async function POST(event) {
const status = await limiter.check(event);
if (status.limited) {
return new Response('Too many requests', {
status: 429,
headers: { 'Retry-After': status.retryAfter.toString() },
});
}
}
Correct IP Detection for Vercel
const limiter = new RateLimiter({
IP: [10, 'h'],
getIP: (event) => event.getClientAddress(),
});
Serverless Security
Vercel Sensitive Environment Variables
vercel env add SECRET_KEY production --sensitive
Neon Connection Limits
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL, {
fetchConnectionCache: true,
});
DoS Risk: Each serverless instance creates connections. Under load, you can exhaust the pool.
Dependency Audit
Check for Vulnerabilities
bun audit
bun audit fix
Critical: Bun Version
bun --version
bun upgrade
Anti-Patterns
Never disable CSRF:
export const auth = betterAuth({
advanced: { disableCSRFCheck: true },
});
Never trust client IP headers without validation:
const ip = request.headers.get('x-forwarded-for');
const ip = event.getClientAddress();
Never use @html without sanitization:
<!-- WRONG -->
{@html userComment}
<!-- RIGHT -->
{@html DOMPurify.sanitize(userComment)}
Never mix pooled and direct Neon connections:
const pooledUrl = process.env.DATABASE_URL;
const directUrl = process.env.DATABASE_URL_DIRECT;
References
- references/sveltekit-csrf.md - CSRF protection details, CVE history
- references/better-auth-vulns.md - Known vulnerabilities, monitoring
- references/injection.md - SQL/Cypher injection patterns
- references/headers.md - CSP, security headers, CORS
- references/rate-limiting.md - sveltekit-rate-limiter configuration
- references/serverless.md - Vercel, Neon security concerns