Content Security Policy for Vibe-Coded Apps: A Practical CSP Guide
Quick answer: Content Security Policy (CSP) is a browser-enforced allowlist that blocks unauthorized scripts, providing the strongest defense against XSS. Most vibe-coded apps ship with no CSP or use 'unsafe-inline', which negates protection. This guide explains how to implement strict policies using nonces, hashes, and report-only mode to secure AI-built applications.
By Gabriel CA · Kraftwire Software
· 10 min readWhat's the Key Takeaway?
Content Security Policy (CSP) is a browser-enforced allowlist that dictates which scripts, styles, and external resources your application is permitted to execute. It serves as the primary defense-in-depth mechanism against Cross-Site Scripting (XSS) · even if an attacker successfully injects malicious code into your database or DOM, a robust CSP prevents that code from running. Most vibe-coded apps ship with no CSP at all, or worse, a policy containing 'unsafe-inline' which effectively disables the protection. To secure your app, you must move beyond these defaults by using nonces or hashes for inline scripts and enforcing a strict default-src 'self' policy.
What Does CSP Actually Do?
CSP is an HTTP response header (or a <meta> tag) that acts as a gatekeeper. When a browser loads your site, it reads the CSP and builds a security context. If the page later tries to load a script from an unauthorized domain or execute an inline event handler that isn't explicitly permitted, the browser blocks the action immediately.
The policy consists of directives that target specific resource types:
In this example, the browser is told:
default-src 'self': Only load resources from your own domain by default.script-src 'self' https://apis.google.com: Only execute scripts from your domain or Google's API domain.img-src 'self' data:: Allow images from your domain and base64-encoded data URIs.
This is critical because XSS relies on the browser's inability to distinguish between the code you wrote and the code an attacker injected. By defining a strict allowlist, you remove the browser's "trust by default" behavior. While input sanitization is your first line of defense, CSP is the safety net that catches what your sanitization logic misses.
Why Do Vibe-Coded Apps Get CSP Wrong?
When tools like Lovable, Bolt.new, or Cursor generate a UI, they often use inline styles or scripts to make the "vibe" work instantly. A strict CSP would break these elements, so the AI either omits the header or uses "unsafe" keywords to ensure the app doesn't break during the preview.
1. The 'unsafe-inline' Trap
This is the most frequent failure. Including 'unsafe-inline' in your script-src tells the browser to allow any script block found in the HTML. Since most XSS attacks involve injecting a <script> tag or an onclick attribute, this keyword nullifies the primary benefit of CSP. If your policy has this, you are effectively unprotected against script injection.
2. The 'unsafe-eval' Risk
Many AI-generated apps rely on older libraries or specific templating engines that use eval() to turn strings into executable code. The 'unsafe-eval' directive re-enables this dangerous behavior. Attackers can leverage this to bypass other security controls by executing logic hidden within strings.
3. Over-reliance on Wildcards
Directives like script-src * or connect-src https: are far too broad. They allow your app to communicate with or load code from any domain on the internet.
4. Missing Object and Base Directives
object-src 'none': Prevents the loading of legacy plugins like Flash or Java applets, which are common vectors for bypassing script restrictions.base-uri 'self': Prevents attackers from injecting a<base>tag to redirect all relative URLs (like your scripts and CSS) to their own malicious server.
How Nonces and Hashes Replace 'unsafe-inline'
To maintain security without breaking the inline scripts that vibe-coding tools often generate, you must use either nonces or hashes.
Using Nonces (Number Used Once)
A nonce is a unique, cryptographically strong random string generated for every single page load. You include it in your CSP header and on every trusted script tag:
Header:
HTML:
Because the attacker cannot predict the nonce for the next request, their injected scripts will lack the correct attribute and be blocked. Note that a hardcoded nonce is as useless as no nonce at all; it must be dynamic.
Using Hashes
If your inline scripts are static and don't change between deployments, you can use a SHA hash. You calculate the SHA-256 hash of the script's content and add it to your policy.
The browser will only execute the inline script if its hash matches exactly. This is ideal for static site generators (SSG) where you cannot generate a per-request nonce. You can use our SRI Hash tool to generate these values.
The Ultimate Vibe Coding Security Checklist: CSP Edition
When moving from a "vibe" to a production-ready application, follow this checklist to ensure your CSP is actually doing its job. This is a core part of a wider application security checklist.
- Set default-src 'self': Ensure everything is denied unless explicitly allowed.
- Remove 'unsafe-inline': Replace with nonces or hashes for all scripts.
- Remove 'unsafe-eval': Audit dependencies to see if
eval()is truly necessary. - Restrict connect-src: List only your backend (e.g., Supabase, Xano, Firebase) and necessary APIs.
- Set object-src 'none': Disable legacy plugin support.
- Set base-uri 'self': Lock down the base URL.
- Add frame-ancestors 'none': Prevent clickjacking by disallowing your site from being iframed.
- Use Report-Only mode first: Test your policy without breaking the user experience.
How to Roll Out CSP Without Breaking Your App
The fear of "breaking the vibe" is why many developers avoid CSP. The solution is Content-Security-Policy-Report-Only. This header tells the browser to monitor the policy and send reports to a specific URL, but not to block any resources.
- Deploy in Report-Only: Send the
Content-Security-Policy-Report-Onlyheader with your desired strict policy. - Monitor Violations: Use a service or a simple endpoint to collect JSON reports from browsers.
- Adjust the Policy: If a legitimate script from a CDN is being flagged, add that CDN to your allowlist.
- Enforce: Once the reports fall to zero, switch the header to
Content-Security-Policy.
This approach ensures that you find gaps in your logs rather than through user complaints. For more on how architecture impacts these risks, see our guide on architecture security risks.
A Practical Starter CSP for AI-Built Apps
If you are using a modern stack like Lovable (React/Vite) with a Supabase backend, this is a solid starting point. It balances strictness with the realities of modern web development.
*Note: style-src 'unsafe-inline' is often necessary for CSS-in-JS libraries. While not ideal, it is significantly less risky than script-src 'unsafe-inline'.*
Deploying the CSP Header
You have two primary ways to deliver your CSP.
1. HTTP Response Headers (Recommended)
This is the most secure method. It is processed before the HTML is even parsed.
- Vercel/Netlify: Add the headers to your
vercel.jsonornetlify.toml. - Cloudflare: Use "Transform Rules" to inject the header at the edge.
- Supabase/Edge Functions: Set the header in your response object.
2. HTML Meta Tag (Fallback)
If you cannot control the server headers (e.g., on some static hosts), use a meta tag in your <head>:
Warning: Meta tags do not support frame-ancestors, report-uri, or sandbox directives. They are a partial solution only.
How to Audit Your Policy
A single typo can make a CSP useless. For example, forgetting the semicolon between directives can cause the browser to ignore the entire policy.
- Use the CSP Evaluator: Paste your URL or header into our CSP Evaluator. It will highlight high-severity bypasses and suggest fixes.
- Check Security Headers: Use our Security Headers tool to see how your CSP interacts with other headers like HSTS and X-Content-Type-Options.
- Full Site Scan: Run a free scan at simplyscan.io. In ~30 seconds, we check for exposed API keys, missing RLS, and CSP weaknesses across your entire app.
A well-implemented CSP is the single most effective way to harden that architecture against the most common web attack vector: XSS.
Related Free Tools
- CSP Generator · Build a custom policy from scratch.
- XSS Prevention Guide · Learn how to pair CSP with proper sanitization.
- Vibe Coding Security Checklist · The full guide to securing AI-generated apps.
- SSL Checker · Ensure your transport layer is as secure as your application layer.
FAQ
Does CSP stop all XSS attacks?
No, CSP is a defense-in-depth layer, not a silver bullet. It is designed to stop the *execution* of malicious scripts if they are successfully injected. You still need robust input sanitization and output encoding to prevent the injection in the first place. A policy with 'unsafe-inline' provides almost no protection against modern XSS. Always pair CSP with a secure architecture.
Will adding a CSP break my vibe-coded app?
It can if the AI used inline scripts or styles. This is why you should always start with Content-Security-Policy-Report-Only. This allows you to see what *would* have broken in your browser's console or a reporting endpoint without actually blocking anything. Fix the issues by adding nonces or allowing specific domains, then switch to full enforcement.
Should I use a meta tag or an HTTP header?
Always prefer the HTTP response header. It is more secure because it applies to the entire response and supports all directives, including frame-ancestors (for clickjacking protection) and report-uri. Use the <meta> tag only as a last resort if you have zero control over your hosting provider's header configurations.
What is the difference between a CSP nonce and a hash?
A nonce is a random string generated for every request; it's best for dynamic apps where the server can inject the nonce into the HTML. A hash is a static signature of a specific script's content; it's best for static sites (SSG) where the script content never changes. Both allow you to run specific inline scripts without using the dangerous 'unsafe-inline'.
Do I still need X-Frame-Options if I have CSP?
frame-ancestors in CSP is the modern replacement for X-Frame-Options. However, for maximum compatibility with very old browsers, many developers still send both. If both are present, the CSP frame-ancestors directive takes precedence in modern browsers. Setting frame-ancestors 'none' is the strongest way to prevent clickjacking today.
How do I know if my CSP is actually working?
The easiest way is to use a specialized tool. SimplyScan's CSP Evaluator will analyze your policy for common bypasses. You can also manually test by trying to inject a simple <script>alert(1)</script> into a search bar or URL parameter; if your CSP is working, the browser console will show a "Refused to execute script" error.
Frequently asked questions
Does CSP stop all XSS attacks?
No, CSP is a defense-in-depth layer. It stops the execution of malicious scripts if they are successfully injected, but it does not prevent the injection itself. You still need robust input sanitization. A policy containing 'unsafe-inline' provides almost no protection against modern XSS attacks. Always pair CSP with a secure architecture.
Will adding a CSP break my vibe-coded app?
It can if the AI used inline scripts or styles that the policy doesn't account for. To avoid breaking your app, always start with the Content-Security-Policy-Report-Only header. This allows you to see violations in the browser console without actually blocking resources. Once you've added nonces or allowed necessary domains, you can switch to full enforcement.
Should I use a meta tag or an HTTP header?
Always prefer the HTTP response header. It is more secure, applies to the entire response, and supports all directives, including frame-ancestors and report-uri. Use the meta tag only as a fallback if you cannot control your server's headers. Note that meta tags are ignored for clickjacking protection (frame-ancestors).
What is the difference between a CSP nonce and a hash?
A nonce is a random string generated for every request, ideal for dynamic apps. A hash is a static signature of a script's content, perfect for static sites (SSG) where script content doesn't change. Both allow you to run specific, trusted inline scripts while keeping the rest of your site protected from unauthorized script execution.
Do I still need X-Frame-Options if I have CSP?
The frame-ancestors directive in CSP is the modern replacement for X-Frame-Options. While X-Frame-Options is still used for legacy browser support, frame-ancestors is more flexible and takes precedence in modern browsers. Setting frame-ancestors 'none' is the most effective way to prevent clickjacking attacks on your application.
How do I know if my CSP is actually working?
You can use SimplyScan's free CSP Evaluator to check for common bypasses and configuration errors. Additionally, you can check your browser's developer console; if the CSP is active, it will log errors whenever a resource is blocked. A truly effective policy should avoid 'unsafe-inline', 'unsafe-eval', and broad wildcards like *.