Implementing Content Security Policy
| Created: | |
| Updated: | |
| Written by: | AI |
AI-assisted content. A human was involved, but the AI did most of the heavy lifting.
Content Security Policy (CSP) is a powerful security mechanism that helps protect websites from cross-site scripting (XSS) attacks, data injection, and other security vulnerabilities. However, implementing a strict CSP without unsafe-inline can be challenging, especially for existing websites with inline styles and scripts.
This article documents the challenges faced and solutions implemented when moving vikan.cloud to a strict CSP policy, eliminating all inline styles and scripts.
What is Content Security Policy?
Content Security Policy is an HTTP header that allows website administrators to control which resources the browser is allowed to load and execute. It acts as a whitelist, blocking unauthorized resources and preventing common attack vectors.
A strict CSP typically includes:
script-src 'self'- Only allow scripts from the same originstyle-src 'self'- Only allow styles from the same origindefault-src 'self'- Default policy for all resource types- No
'unsafe-inline'- Blocks inline scripts and styles
The Challenge: Moving Away from Inline Styles and Scripts
Modern web frameworks and development practices often rely on inline styles and scripts for convenience. However, these violate CSP when 'unsafe-inline' is not allowed. The main challenges include:
1. Framework-Generated Inline Styles
Many frameworks, including Astro, generate scoped styles that are injected as inline <style> blocks. While these are safe from a security perspective, they violate strict CSP policies.
Solution: Move all component styles to external CSS files (global.css) and use CSS classes or CSS custom properties for dynamic styling.
2. JavaScript Inline Style Manipulation
JavaScript code that dynamically sets styles using element.style.property = value violates CSP.
Example of problematic code:
// ❌ Violates CSP
header.style.cursor = 'pointer';
header.style.userSelect = 'none';
tagsList.style.display = 'flex';
Solution: Replace inline style assignments with CSS classes:
// ✅ CSP-compliant
header.classList.add('cursor-pointer', 'select-none');
tagsList.classList.remove('hidden');
tagsList.classList.add('flex');
3. Dynamic Canvas Sizing
Canvas elements often require dynamic sizing based on viewport dimensions, which traditionally uses inline styles.
Solution: Use CSS custom properties (CSS variables) set via setProperty(), which are CSP-compliant when referenced in CSS:
// ✅ CSP-compliant - CSS custom properties
canvas.style.setProperty('--canvas-width', width + 'px');
canvas.style.setProperty('--canvas-height', height + 'px');
/* Reference in CSS */
#game-canvas {
width: var(--canvas-width, 800px);
height: var(--canvas-height, 600px);
}
Benefits of Strict CSP
Implementing a strict CSP without 'unsafe-inline' provides several security and development benefits:
Security Benefits
- XSS Protection: Prevents inline script injection attacks
- Data Injection Prevention: Blocks unauthorized code execution
- Reduced Attack Surface: Limits the ways malicious code can be injected
- Compliance: Meets security scanning requirements and best practices
Development Benefits
- Better Code Organization: Forces separation of concerns (HTML, CSS, JavaScript)
- Improved Maintainability: External files are easier to manage and version control
- Better Caching: External CSS and JS files can be cached by browsers
- Easier Debugging: Styles and scripts are in predictable locations
- Code Reusability: External files can be shared across pages
Implementation Checklist
When implementing strict CSP, follow this checklist:
CSS and Styling
- ✅ No inline
style="..."attributes in HTML elements - ✅ No
<style>blocks in components (exceptis:globalif absolutely necessary) - ✅ All component-specific styles moved to
global.cssor dedicated CSS files - ✅ No
!importantflags (use CSS specificity instead) - ✅ Tailwind CSS classes used for utility-first styling
- ✅ CSS custom properties used for dynamic theming
JavaScript
- ✅ No inline
<script>tags withoutis:inlinedirective - ✅ All JavaScript in external files (preferably in
/public/scripts/) - ✅ No
element.style.propertyassignments - use CSS classes instead - ✅ Dynamic styling uses CSS custom properties when needed
- ✅ Scripts are CSP-compliant
Framework-Specific Considerations
Astro Framework:
- Use
is:inlinefor scripts that must be inline (e.g., theme initialization) - Move all
<style is:global>blocks to external CSS files - Use CSS classes instead of inline styles for dynamic content
- Handle Vite’s development mode style injection with conditional CSP (see “Development vs Production Environments” section)
Troubleshooting CSP Violations
When implementing CSP, violations will appear in the browser console. Here’s how to identify and fix them:
Using Browser Developer Tools
- Open Developer Tools: Press
F12or right-click → Inspect - Navigate to Console Tab: Look for CSP violation errors
- Check Network Tab: Verify resources are loading from allowed sources
Common CSP Violation Messages
Inline Style Violation:
Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self'"
Solution: Move the inline style to an external CSS file or use CSS classes.
Inline Script Violation:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'"
Solution: Move the script to an external file or use is:inline directive (if absolutely necessary).
External Resource Violation:
Refused to load the image 'https://example.com/image.jpg' because it violates the following Content Security Policy directive: "img-src 'self'"
Solution: Add the domain to img-src directive or host the image on your domain.
Step-by-Step Troubleshooting Process
-
Identify the Violation:
- Open browser console (F12)
- Look for CSP violation errors
- Note the resource type (style, script, image, etc.)
-
Locate the Source:
- Check the error message for the file/line number
- Use browser “Inspect Element” to find the problematic code
- Search codebase for the violating pattern
-
Fix the Violation:
- Move inline styles to external CSS files
- Replace
element.style.propertywith CSS classes - Move inline scripts to external files
- Use CSS custom properties for dynamic styling
-
Verify the Fix:
- Clear browser cache
- Reload the page
- Check console for remaining violations
- Test functionality to ensure nothing broke
Example: Finding Inline Style Violations
Using grep to find inline styles:
# Find inline style attributes
grep -r 'style=' src/
# Find JavaScript style assignments
grep -r '\.style\.' public/scripts/ src/scripts/
# Find style blocks
grep -r '<style' src/ | grep -v 'is:global'
Using Browser Console:
- Open Developer Tools (F12)
- Go to Console tab
- Look for CSP violation errors
- Click on the error to see the source element
- Use “Inspect Element” to locate the code
Example: Finding Inline Script Violations
Using grep:
# Find inline script tags
grep -r '<script' src/ | grep -v 'is:inline'
# Find eval() calls (also violates CSP)
grep -r 'eval(' public/scripts/ src/scripts/
Using Browser Console:
- CSP violations for scripts will show in console
- Check Network tab for blocked script resources
- Verify scripts are loaded from allowed sources
Real-World Example: vikan.cloud Migration
When migrating vikan.cloud to strict CSP, we encountered and fixed the following violations:
1. Table Sorting Script
Problem: JavaScript was setting inline styles for cursor and user selection.
// Before (violates CSP)
header.style.cursor = 'pointer';
header.style.userSelect = 'none';
Solution: Replaced with CSS classes.
// After (CSP-compliant)
header.classList.add('cursor-pointer', 'select-none');
2. Theme Toggle Icons
Problem: JavaScript was toggling icon visibility using inline styles.
// Before (violates CSP)
sunIcon.style.display = 'none';
moonIcon.style.display = 'block';
Solution: Replaced with CSS classes.
// After (CSP-compliant)
sunIcon.classList.add('hidden');
sunIcon.classList.remove('block');
moonIcon.classList.remove('hidden');
moonIcon.classList.add('block');
3. Dynamic Canvas Sizing
Problem: Canvas element required dynamic width/height based on viewport.
Solution: Used CSS custom properties.
// JavaScript (CSP-compliant)
canvas.style.setProperty('--canvas-width', width + 'px');
canvas.style.setProperty('--canvas-height', height + 'px');
/* CSS */
#game-canvas {
width: var(--canvas-width, 800px);
height: var(--canvas-height, 600px);
}
Development vs Production Environments
When implementing CSP, it’s important to handle development and production environments differently. Development tools like Vite’s Hot Module Replacement (HMR) and style injection require more permissive CSP policies.
The Challenge: Framework Development Tools
Modern build tools inject inline styles and scripts during development:
- Vite: Injects
<style data-vite-dev-id=...>tags for component styles - HMR: Uses WebSocket connections (
ws:,wss:) for live reloading - Dev Tools: May require inline scripts for debugging
These are safe in development but violate strict CSP policies.
Solution: Conditional CSP Based on Environment
Use environment detection to apply different CSP policies:
// src/middleware.ts
export const onRequest: MiddlewareHandler = async (context, next) => {
const response = await next();
const headers = new Headers(response.headers);
// Detect environment
const isDev = import.meta.env.DEV;
// Conditional CSP based on environment
const csp = [
"default-src 'self'",
// Allow unsafe-inline in dev for Vite HMR and style injection
isDev ? "script-src 'self' 'unsafe-inline'" : "script-src 'self'",
isDev ? "style-src 'self' 'unsafe-inline'" : "style-src 'self'",
"img-src 'self'",
"font-src 'self'",
"object-src 'none'",
// Allow WebSocket in dev for Vite HMR
isDev ? "connect-src 'self' ws: wss:" : "connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
].join('; ');
headers.set('Content-Security-Policy', csp);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: headers,
});
};
Environment-Specific Policies
Development Mode (npm run dev):
- ✅ Allows
'unsafe-inline'for scripts and styles - ✅ Allows WebSocket connections (
ws:,wss:) for HMR - ✅ Permissive enough for development tools to work
Production Mode (npm run build):
- ✅ Strict CSP without
'unsafe-inline' - ✅ No WebSocket connections allowed
- ✅ Maximum security for end users
Why This Approach Works
- Development: Framework tools can inject styles/scripts without CSP violations
- Production: Styles are bundled into external CSS files (no inline styles)
- Security: Production maintains strict CSP while development remains functional
- Best Practice: Common pattern used by many modern frameworks
Verifying Production Builds
Always verify that production builds don’t contain inline styles:
# Check production build for inline styles
grep -r "data-vite-dev-id" dist/ || echo "No Vite dev styles in production"
# Check for inline style tags
grep -r "<style" dist/ | grep -v "\.css" || echo "No inline styles found"
Production builds should bundle all styles into external CSS files, making strict CSP possible.
Best Practices & Conclusion
Best Practices
- Start Strict: Begin with a strict CSP and relax only when necessary
- Environment-Aware: Use conditional CSP for development vs production
- Use Report-Only Mode: Test CSP in
Content-Security-Policy-Report-Onlymode first - Monitor Violations: Set up CSP reporting to catch violations in production
- Document Exceptions: If
'unsafe-inline'is absolutely necessary, document why - Regular Audits: Periodically audit codebase for CSP violations
- Automated Checks: Add CSP violation checks to CI/CD pipeline
- Verify Production: Always verify production builds don’t contain inline styles
Conclusion
Implementing strict CSP without 'unsafe-inline' requires careful planning and refactoring, but the security and development benefits are significant. By moving all styles and scripts to external files, using CSS classes for dynamic styling, and leveraging CSS custom properties, websites can achieve strong security while maintaining functionality.
The key is to identify violations early using browser developer tools, systematically refactor code to be CSP-compliant, and establish processes to prevent future violations.
Related Articles: