CSRF Protection & Security Headers: The Missing Layer in AI-Built Apps
AI-generated apps almost never set security headers or CSRF protection. Here's what headers you need and how to add them.
By Daniel A · Kraftwire Software
· 9 min readThe Invisible Security Layer
Security headers are HTTP response headers that tell browsers how to behave when handling your site's content. They prevent entire classes of attacks, including clickjacking, MIME sniffing, protocol downgrade, and XSS, with zero changes to your application code.
The problem is that AI coding tools almost never add them. When you ask an AI to build your app, it focuses on features and functionality. Security headers are an infrastructure concern that falls outside what code generators typically handle. The result is that most AI-built apps ship with none of these protections in place.
This guide explains what each header does, why it matters, and exactly how to add them to your application.
Why Security Headers Matter
Security headers are your first line of defense. They work at the browser level, which means they protect your users even if your application code has vulnerabilities. Think of them as guardrails that prevent certain types of attacks from being possible in the first place.
Without security headers, your app is relying entirely on your code being perfect. With security headers, even if you have a bug that would normally be exploitable, the browser blocks the attack before it can succeed.
Essential Security Headers
1. Content-Security-Policy (CSP)
CSP is the most powerful security header. It tells the browser which sources of content are allowed to load on your page. This prevents XSS attacks, data injection, and unauthorized resource loading.
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.yourdomain.com;
**What this does:**
`default-src 'self'` blocks all resources from external domains by default
`script-src 'self'` only allows scripts from your own domain (blocks injected scripts)
`style-src 'self' 'unsafe-inline'` allows your styles and inline styles (some frameworks need this)
`img-src 'self' data: https:` allows images from your domain, data URIs, and any HTTPS source
`connect-src 'self'` restricts which APIs your frontend can call
**Why it matters:** Without CSP, if an attacker manages to inject a script tag into your page (through a stored XSS vulnerability, for example), that script can load additional malicious code from any domain. With CSP, the browser blocks scripts from unauthorized sources even if they make it into your HTML.
**How to implement CSP gradually:**
Start with `Content-Security-Policy-Report-Only` to log violations without blocking anything
Review the violations to understand what your app actually needs
Build your policy based on the legitimate sources
Switch from Report-Only to enforcing mode
2. Strict-Transport-Security (HSTS)
HSTS forces browsers to use HTTPS for all future requests to your domain:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
**What this does:** After a browser receives this header once, it will automatically convert all HTTP requests to your domain into HTTPS for the next year (31,536,000 seconds). This prevents man-in-the-middle attacks where an attacker on the same network downgrades the connection from HTTPS to HTTP.
**Why it matters:** Even if your server redirects HTTP to HTTPS, there is a brief moment during the first request where the connection is unencrypted. An attacker can intercept this initial request and serve a fake version of your site. HSTS eliminates this window by telling the browser to never even try HTTP.
**The preload directive:** Adding `preload` and submitting your domain to hstspreload.org puts your domain on a list that is built into browsers. This means even the very first visit to your site will use HTTPS, with no initial HTTP request at all.
3. X-Frame-Options
Prevents your site from being embedded in an iframe on another site, which is the basis of clickjacking attacks:
X-Frame-Options: DENY
**What is clickjacking?** An attacker creates a page with an invisible iframe containing your app. They position buttons on the attacker's page so that when the user clicks what they think is a harmless button, they are actually clicking a button in your hidden app. This can trick users into changing account settings, making purchases, or granting permissions.
**When to use SAMEORIGIN instead of DENY:** If your app needs to be iframed by other pages on your own domain (like embedding a widget), use `X-Frame-Options: SAMEORIGIN` instead. This allows same-domain framing while blocking cross-origin framing.
4. X-Content-Type-Options
Prevents browsers from guessing (sniffing) the MIME type of responses:
X-Content-Type-Options: nosniff
**Why this matters:** Without this header, if an attacker manages to upload a file with a `.jpg` extension but containing JavaScript, the browser might "sniff" the content, determine it is actually JavaScript, and execute it. The `nosniff` directive tells the browser to trust the Content-Type header and not try to guess.
5. Referrer-Policy
Controls how much referrer information is sent with requests when users navigate away from your site:
Referrer-Policy: strict-origin-when-cross-origin
**Why this matters:** By default, browsers send the full URL (including query parameters) as the Referrer header when users click links. If your URLs contain sensitive information like session tokens, user IDs, or search queries, this data gets leaked to every external site your users visit.
**The recommended value** (`strict-origin-when-cross-origin`) sends the full URL for same-origin requests (useful for analytics) but only the origin (domain) for cross-origin requests (protecting user privacy).
6. Permissions-Policy
Controls which browser features your app can use:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
**What this does:** This tells the browser that your app does not need access to the camera, microphone, location, or payment APIs. If an attacker injects code that tries to access these features, the browser will block the request.
**When to customize:** If your app legitimately uses the camera or microphone, change the value for those specific features to allow your domain: `camera=(self)`.
What Is CSRF?
Cross-Site Request Forgery (CSRF) tricks a logged-in user's browser into making unwanted requests to your application. It is a separate attack from XSS, but security headers help prevent it.
How CSRF Works
User logs into your app (session cookie is set in their browser)
User visits a malicious site while still logged in
The malicious site contains a hidden form or script that sends a request to your app
The browser automatically includes the session cookie with the request
Your app processes the request as if the user intended it
**Example attack:** An attacker creates a page with an invisible form that submits a password change request to your app. When a logged-in user visits the attacker's page, their browser submits the form with their session cookie, and their password gets changed without their knowledge.
CSRF Prevention
**Method 1: CSRF Tokens**
// Generate a unique token per session
const csrfToken = crypto.randomUUID();
// Include it in forms as a hidden field
<input type="hidden" name="csrf_token" value={csrfToken} />
// Validate on the server before processing the request
if (request.body.csrf_token !== session.csrfToken) {
return new Response("Forbidden", { status: 403 });
}
The malicious site cannot read the CSRF token from your page (because of the same-origin policy), so it cannot include it in its forged request.
**Method 2: SameSite Cookie Attribute**
For API-based apps that use cookies for authentication, the SameSite attribute is the simplest CSRF protection:
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
`SameSite=Strict` prevents the cookie from being sent on any cross-origin request
`SameSite=Lax` allows the cookie on top-level navigation (clicking a link) but blocks it on form submissions and API calls from other sites
`Secure` ensures the cookie is only sent over HTTPS
`HttpOnly` prevents JavaScript from reading the cookie (XSS protection)
**Method 3: Custom Request Headers**
For AJAX-based applications, require a custom header on all state-changing requests:
// Frontend
fetch("/api/update", {
method: "POST",
headers: { "X-Requested-With": "XMLHttpRequest" },
});
// Server - reject requests without the custom header
if (!request.headers.get("X-Requested-With")) {
return new Response("Forbidden", { status: 403 });
}
Cross-origin requests cannot set custom headers without CORS approval, so this effectively blocks CSRF.
How to Add Security Headers
Vercel (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
]
}
]
}
Netlify (_headers file)
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Express.js (using helmet)
const helmet = require("helmet");
app.use(helmet());
The `helmet` package sets all common security headers with sensible defaults in a single line.
Next.js (next.config.js)
const securityHeaders = [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
];
module.exports = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
Testing Your Headers
After adding security headers, verify they are working:
**Browser DevTools:** Open your deployed app, go to the Network tab, click on any request, and check the Response Headers section
**SecurityHeaders.com:** Enter your URL and get a grade with specific recommendations
**SimplyScan:** Our security scan checks for missing headers as part of the full audit
Scan for Missing Headers
SimplyScan's free scan checks 3 core categories (Secrets, Frontend, Supabase). Go Pro for all 14 categories including CSRF and security headers, with 40+ checks that detect missing headers, permissive CORS, and CSRF vulnerabilities.
[Scan your app now](/)
Related Guides
[XSS Prevention Guide](/blog/xss-prevention-guide)
[Code Injection Prevention](/blog/code-injection-prevention)
[Architecture Security Risks](/blog/architecture-security-risks)
[Is Bolt.new Safe?](/blog/is-bolt-safe)