Sign In

Sign in with your preferred provider:

← Back to Articles

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 origin
  • style-src 'self' - Only allow styles from the same origin
  • default-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

  1. XSS Protection: Prevents inline script injection attacks
  2. Data Injection Prevention: Blocks unauthorized code execution
  3. Reduced Attack Surface: Limits the ways malicious code can be injected
  4. Compliance: Meets security scanning requirements and best practices

Development Benefits

  1. Better Code Organization: Forces separation of concerns (HTML, CSS, JavaScript)
  2. Improved Maintainability: External files are easier to manage and version control
  3. Better Caching: External CSS and JS files can be cached by browsers
  4. Easier Debugging: Styles and scripts are in predictable locations
  5. 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 (except is:global if absolutely necessary)
  • ✅ All component-specific styles moved to global.css or dedicated CSS files
  • ✅ No !important flags (use CSS specificity instead)
  • ✅ Tailwind CSS classes used for utility-first styling
  • ✅ CSS custom properties used for dynamic theming

JavaScript

  • ✅ No inline <script> tags without is:inline directive
  • ✅ All JavaScript in external files (preferably in /public/scripts/)
  • ✅ No element.style.property assignments - use CSS classes instead
  • ✅ Dynamic styling uses CSS custom properties when needed
  • ✅ Scripts are CSP-compliant

Framework-Specific Considerations

Astro Framework:

  • Use is:inline for 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

  1. Open Developer Tools: Press F12 or right-click → Inspect
  2. Navigate to Console Tab: Look for CSP violation errors
  3. 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

  1. Identify the Violation:

    • Open browser console (F12)
    • Look for CSP violation errors
    • Note the resource type (style, script, image, etc.)
  2. 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
  3. Fix the Violation:

    • Move inline styles to external CSS files
    • Replace element.style.property with CSS classes
    • Move inline scripts to external files
    • Use CSS custom properties for dynamic styling
  4. 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:

  1. Open Developer Tools (F12)
  2. Go to Console tab
  3. Look for CSP violation errors
  4. Click on the error to see the source element
  5. 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

  1. Development: Framework tools can inject styles/scripts without CSP violations
  2. Production: Styles are bundled into external CSS files (no inline styles)
  3. Security: Production maintains strict CSP while development remains functional
  4. 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

  1. Start Strict: Begin with a strict CSP and relax only when necessary
  2. Environment-Aware: Use conditional CSP for development vs production
  3. Use Report-Only Mode: Test CSP in Content-Security-Policy-Report-Only mode first
  4. Monitor Violations: Set up CSP reporting to catch violations in production
  5. Document Exceptions: If 'unsafe-inline' is absolutely necessary, document why
  6. Regular Audits: Periodically audit codebase for CSP violations
  7. Automated Checks: Add CSP violation checks to CI/CD pipeline
  8. 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:

← Back to Articles