XSS in SvelteKit Applications
Svelte auto-escapes expressions in curly braces, but the {@html} tag renders raw HTML without sanitization. SvelteKit's server load functions and form actions add data-flow paths where unsanitized content can reach the template and bypass Svelte's default protections.
Scan Your SvelteKit AppHow XSS Manifests in SvelteKit
The {@html} tag in Svelte is the primary XSS vector, rendering raw HTML directly into the DOM. Developers use it for rich text, markdown, and embedded content from CMS systems. SvelteKit load functions fetch data server-side and pass it to components. If the data contains user HTML that gets rendered with {@html}, the XSS is introduced at the server level and sent to every client. Form action responses that include user data can also create reflected XSS if the data flows into {@html} tags. Dynamic attribute bindings in Svelte do not prevent javascript: URLs in href attributes.
Real-World Impact
A SvelteKit documentation site used {@html} to render user-contributed code examples and explanations. An attacker submitted a tutorial containing a malicious script disguised within a code block. The {@html} rendering executed the script, which modified all download links on the page to point to malware-infected files.
Step-by-Step Fix
Sanitize content for @html
Create a sanitization utility and use it before rendering with @html.
<!-- UNSAFE -->
{@html userContent}
<!-- SAFE -->
<script lang="ts">
import DOMPurify from 'isomorphic-dompurify';
export let userContent: string;
$: sanitized = DOMPurify.sanitize(userContent, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
});
</script>
{@html sanitized}Sanitize in load functions
Clean HTML data at the server level before it reaches components.
// src/routes/blog/[slug]/+page.server.ts
import DOMPurify from 'isomorphic-dompurify';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const post = await getPost(params.slug);
return {
...post,
content: DOMPurify.sanitize(post.content),
};
};Add CSP headers via hooks
Use SvelteKit hooks to add security headers to every response.
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
);
response.headers.set('X-Content-Type-Options', 'nosniff');
return response;
};Validate URL bindings
Create a helper to validate URLs used in href attributes.
<script lang="ts">
function safeUrl(url: string): string {
try {
const parsed = new URL(url);
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol)
? url : '#';
} catch {
return url.startsWith('/') ? url : '#';
}
}
export let userUrl: string;
</script>
<a href={safeUrl(userUrl)}>User Link</a>Prevention Best Practices
1. Avoid {@html} with user-controlled data. Use text interpolation {expression} which auto-escapes. 2. If {@html} is necessary, sanitize with DOMPurify before passing to the template. 3. Validate URLs in href bindings to block javascript: protocols. 4. Add CSP headers using SvelteKit hooks (handle function in hooks.server.ts). 5. Sanitize data in load functions before returning to components.
How to Test
1. Search for {@html in your Svelte components: grep -r "{@html" --include="*.svelte" 2. Test each {@html} usage with <img src=x onerror=alert(1)> payloads. 3. Check href bindings for javascript: URL injection. 4. Trace load function data flows to {@html} usage. 5. Use Vibe App Scanner to automatically detect XSS patterns in your SvelteKit application.
Frequently Asked Questions
Does Svelte automatically prevent XSS?
Svelte auto-escapes expressions in curly braces {expression}, which prevents most XSS. However, the {@html} tag renders raw HTML without escaping, and href attribute bindings do not block javascript: URLs. Always sanitize content before using {@html}.
Is {@html} in Svelte the same as v-html in Vue?
Yes, functionally they are equivalent. Both render raw HTML strings into the DOM without escaping. Both require sanitization with a library like DOMPurify when used with user-controlled content.
How do I add security headers in SvelteKit?
Use the handle hook in src/hooks.server.ts to modify response headers for every request. You can set Content-Security-Policy, X-Content-Type-Options, and other security headers there. For adapter-specific deployments, you may also need to configure headers in your hosting platform.
Related Security Resources
Is Your SvelteKit App Vulnerable to XSS?
VAS automatically scans for xss vulnerabilities in SvelteKit applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your SvelteKit stack.