Why Content Security Policy Matters More Than Ever
Cross-site scripting (XSS) has been on the OWASP Top 10 list for over a decade. According to a 2024 report by Verizon, web application attacks account for roughly 26% of all data breaches, and injection-based attacks—including XSS—remain among the top three attack vectors. Despite growing awareness, a staggering number of websites still ship without any meaningful client-side security controls.
This is where Content Security Policy (CSP) comes in. CSP is an HTTP response header that instructs the browser on exactly which resources are permitted to load and execute on a page. Think of it as a bouncer for your website: if a script, stylesheet, or image isn’t on the guest list, it doesn’t get in.
But CSP is only one piece of the puzzle. A robust security posture requires a suite of HTTP security headers working in concert. In this guide, we’ll walk through everything you need to know—from CSP fundamentals and directive syntax to complementary headers and real-world deployment strategies.
Understanding How CSP Works
The Basic Mechanism
When a browser receives an HTTP response, it checks for a Content-Security-Policy header. If one is present, the browser parses its directives and enforces them for every resource request the page makes. Any resource that violates the policy is blocked, and the violation is logged in the browser’s developer console.
Here is the simplest possible CSP header:
Content-Security-Policy: default-src 'self'
This single line tells the browser: “Only load resources (scripts, images, styles, fonts, frames—everything) from the same origin as this page.” Any attempt to load a resource from a third-party domain, or to execute an inline script, will be blocked.
Why Default Permissiveness Is Dangerous
Without a CSP, browsers follow a permissive-by-default model. Any script tag injected into your HTML—whether by an attacker exploiting a stored XSS vulnerability or through a compromised third-party library—will execute without question. CSP flips this model to restrictive-by-default, which is a fundamentally safer posture.
Anatomy of a Content Security Policy
A CSP header is composed of directives, each controlling a specific resource type. Directives are separated by semicolons, and each directive contains one or more source expressions.
Key CSP Directives
| Directive | Controls | Example |
|---|---|---|
default-src | Fallback for all resource types | default-src 'self' |
script-src | JavaScript sources | script-src 'self' https://cdn.example.com |
style-src | CSS stylesheets | style-src 'self' 'unsafe-inline' |
img-src | Images | img-src 'self' data: https: |
font-src | Web fonts | font-src 'self' https://fonts.gstatic.com |
connect-src | AJAX, WebSocket, fetch | connect-src 'self' https://api.example.com |
frame-src | Iframes | frame-src https://www.youtube.com |
object-src | Plugins (Flash, Java) | object-src 'none' |
base-uri | Restricts <base> element | base-uri 'self' |
form-action | Form submission targets | form-action 'self' |
frame-ancestors | Who can embed your page | frame-ancestors 'none' |
report-uri / report-to | Violation reporting endpoint | report-uri /csp-report |
Source Expressions Explained
'self'— Same origin only (scheme + host + port must match)'none'— Block everything for this directive'unsafe-inline'— Allow inline scripts/styles (weakens CSP significantly)'unsafe-eval'— Alloweval()and similar constructs'nonce-<base64>'— Allow specific inline elements tagged with a matching nonce'strict-dynamic'— Trust scripts loaded by already-trusted scriptshttps:— Any source served over HTTPSdata:— Allowdata:URIs- Specific hosts — e.g.,
https://cdn.jsdelivr.net
Building a Real-World CSP: Step-by-Step
Let’s build a CSP for a typical business website running on WordPress or Prestashop—the kind of project the team at Lueur Externe handles regularly. This site uses Google Analytics, Google Fonts, a YouTube embed, and a contact form.
Step 1: Start With a Restrictive Baseline
Content-Security-Policy:
default-src 'none';
script-src 'self';
style-src 'self';
img-src 'self';
font-src 'self';
connect-src 'self';
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
This policy blocks everything except same-origin resources. It’s our starting point—intentionally strict so we can selectively open doors.
Step 2: Allow Third-Party Services
Now we add the specific third-party domains our site actually needs:
Content-Security-Policy:
default-src 'none';
script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' https://www.google-analytics.com data:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://www.google-analytics.com https://analytics.google.com;
frame-src https://www.youtube.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Step 3: Handle Inline Scripts Safely
Many CMS platforms—WordPress and Prestashop included—generate inline scripts and styles. Instead of resorting to 'unsafe-inline' (which essentially negates most XSS protection), use nonces or hashes.
Nonce-based approach:
Your server generates a unique, cryptographically random nonce for every page load and includes it in both the CSP header and the inline script tags:
<!-- In your HTML -->
<script nonce="r4nd0mN0nc3V4lu3">
console.log('This inline script is allowed');
</script>
/* In your CSP header */
script-src 'self' 'nonce-r4nd0mN0nc3V4lu3' https://www.googletagmanager.com;
The nonce must be different on every request. If an attacker injects a script, it won’t have the correct nonce and will be blocked.
Step 4: Deploy in Report-Only Mode First
This is critical. Before enforcing your policy, deploy it using the Content-Security-Policy-Report-Only header:
Content-Security-Policy-Report-Only:
default-src 'none';
script-src 'self' 'nonce-abc123' https://www.googletagmanager.com;
...
report-uri /csp-violation-report;
This lets you collect violation reports without breaking a single page. Monitor for a week or two, review the reports, adjust your policy, and only then switch to full enforcement.
Beyond CSP: Essential Companion Security Headers
CSP is powerful, but it should never work alone. Here are the other HTTP security headers every site should implement:
Strict-Transport-Security (HSTS)
Forces browsers to use HTTPS for all future requests:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
The max-age value of 63,072,000 seconds equals two years. Adding preload lets you submit your domain to the browser’s HSTS preload list, ensuring HTTPS-only access even on the very first visit.
X-Content-Type-Options
Prevents MIME-type sniffing, which attackers can exploit to execute disguised files:
X-Content-Type-Options: nosniff
X-Frame-Options
While frame-ancestors in CSP largely replaces this header, it’s still wise to include for older browser compatibility:
X-Frame-Options: DENY
Referrer-Policy
Controls how much referrer information is sent with outgoing requests:
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy
Restricts access to browser features like camera, microphone, geolocation:
Permissions-Policy: camera=(), microphone=(), geolocation=()
Implementation: Where to Add Your Headers
Depending on your stack, you’ll add these headers in different places.
Apache (.htaccess or httpd.conf)
<IfModule mod_headers.c>
Header set Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-ancestors 'none';"
Header set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "DENY"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>
Nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-ancestors 'none';" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
AWS CloudFront (Response Headers Policy)
For sites hosted on AWS—an area where Lueur Externe holds AWS Solutions Architect certification—you can configure security headers directly through CloudFront Response Headers Policies in the AWS console or via CloudFormation/Terraform. This approach is particularly elegant because it applies headers at the CDN edge, covering all origin types (S3, ALB, custom origin) uniformly.
WordPress and Prestashop Plugins
If server-level configuration isn’t accessible, plugins can add headers at the application layer:
- WordPress: Plugins like HTTP Headers or Really Simple Security offer GUI-based CSP configuration.
- Prestashop: Custom modules or the
config/config.inc.phpfile can inject headers via PHP’sheader()function.
However, server-level implementation is always preferred for performance and reliability.
Common CSP Mistakes (and How to Avoid Them)
1. Using 'unsafe-inline' and 'unsafe-eval' as a Shortcut
These directives essentially disable CSP’s main protection against XSS. According to Google’s research, over 94% of CSP policies that allow 'unsafe-inline' in script-src are bypassable. Always use nonces or hashes instead.
2. Overly Broad Whitelisting
Allowing an entire CDN domain like https://cdn.jsdelivr.net in script-src means any of the millions of packages hosted there could be loaded. If an attacker finds a way to inject a script tag pointing to a malicious library on that CDN, your CSP won’t stop it. Use 'strict-dynamic' combined with nonces to mitigate this.
3. Forgetting About default-src
If you define script-src but forget object-src, the browser falls back to default-src. If default-src isn’t set either, plugins and objects are unrestricted. Always start with default-src 'none' and explicitly open each directive you need.
4. Not Monitoring Violations
A CSP without a report-uri or report-to directive is flying blind. You won’t know if your policy is blocking legitimate resources or if real attacks are being attempted. Set up a reporting endpoint—services like Report URI, Sentry, or a custom logging endpoint work well.
5. Set-and-Forget Mentality
Your website evolves. New third-party integrations, updated plugins, redesigned pages—all can introduce new resource requirements. Review your CSP quarterly at minimum.
Measuring Your Security Headers
After implementation, test your headers using these free tools:
- SecurityHeaders.com — Grades your site from A+ to F based on header presence and configuration
- CSP Evaluator (Google) — Analyzes your CSP for known bypasses
- Browser DevTools — The Console tab shows CSP violations in real time
- Mozilla Observatory — Comprehensive security scan including headers, TLS, and more
Aim for an A+ rating on SecurityHeaders.com. According to a 2024 scan by Scott Helme (the tool’s creator), fewer than 8% of the top 1 million websites achieve this grade—which means proper security headers are a genuine competitive differentiator.
The Performance Question: Do Security Headers Slow Down My Site?
Short answer: no. Security headers add a negligible number of bytes to your HTTP responses—typically less than 500 bytes total. There is zero rendering or JavaScript execution overhead. In fact, a well-configured CSP can improve perceived performance by preventing unauthorized scripts from loading and consuming bandwidth.
The only caveat is nonce generation on the server side, which requires a cryptographically secure random number generator. On modern servers, this takes microseconds—completely imperceptible.
CSP Level 3 and the Future
The latest evolution, CSP Level 3, introduces several improvements:
'strict-dynamic'— Propagates trust from a nonced/hashed script to any scripts it dynamically loads. This dramatically simplifies CSP for modern JavaScript applications that load modules dynamically.'report-to'— Replaces the deprecatedreport-uriwith the Reporting API, enabling structured JSON reports and batch delivery.'unsafe-hashes'— Allows specific inline event handlers (likeonclick) by their hash, without opening the floodgates of'unsafe-inline'.
These features make CSP more practical for complex, modern web applications—including single-page apps (SPAs) built with React, Vue, or Angular.
Conclusion: Security Headers Are Not Optional
Configuring Content Security Policy and companion security headers is no longer a “nice to have”—it’s a fundamental requirement for any professional web presence. With XSS attacks becoming more sophisticated and browser-based threats evolving constantly, CSP provides a powerful, standards-based defense layer that costs nothing to implement and significantly raises the bar for attackers.
The key takeaways:
- Start with
default-src 'none'and build up - Use nonces instead of
'unsafe-inline' - Deploy in report-only mode before enforcing
- Combine CSP with HSTS, X-Content-Type-Options, and other headers
- Monitor violations and review your policy regularly
At Lueur Externe, we’ve been helping businesses secure their web applications since 2003. As certified Prestashop experts and AWS Solutions Architects based in the Alpes-Maritimes (06), we integrate security headers into every project we deliver—whether it’s a WordPress site, a Prestashop e-commerce store, or a custom cloud-hosted application. If you’d like a professional security audit of your website’s headers or need help implementing a bulletproof CSP, get in touch with our team. Your users—and your reputation—deserve it.