First week of a new policy

Mixed content

A secure page loading something over http://. It is the least dramatic problem on this list and the one you are most likely to actually have — usually an image URL typed in 2016, in a CMS field nobody has opened since.

It is also the easiest first win with CSP reporting: the reports are unambiguous, the fix is a one-line edit, and you can watch the issue stop appearing.

What mixed content is, and why it still matters

A page served over HTTPS that fetches a subresource over HTTP has mixed content. The padlock is about the document; anything it loads insecurely travels in the clear, and anyone on the network path can read it or change it before it arrives.

For a script the consequence is total: whoever controls the path controls the code, and the code runs on your origin. Browsers took that seriously years ago and now refuse insecure scripts, frames and network calls on a secure page outright. Images, audio and video, once allowed with a warning, are now upgraded to HTTPS automatically and dropped if that fails.

Which sounds like the problem solved, and mostly it is — for the visitor. What the browser's own handling does not do is tellyou. The image quietly disappears, or it does not, depending on whether your CDN happens to answer on HTTPS. A reporting policy is how you find the URLs before a visitor does.

What the report looks like

A source expression written https://cdn.example.com names a scheme as well as a host, so the same host overhttp:// does not match it. That mismatch is what produces this:

A legacy image URL on a product page, under a policy that allows the same CDN over HTTPS.
{
  "csp-report": {
    "document-uri": "https://shop.example.com/products/1421",
    "referrer": "https://shop.example.com/",
    "violated-directive": "img-src",
    "effective-directive": "img-src",
    "original-policy": "default-src 'self'; img-src 'self' https://cdn.example.com; report-uri https://ingest.headerhawk.com/r/abc123",
    "disposition": "report",
    "blocked-uri": "http://cdn.example.com/legacy/size-guide.png",
    "status-code": 200,
    "script-sample": ""
  }
}

How it appears in HeaderHawk

img-src blocking cdn.example.com

  • blocked-uri carries the scheme, and it is the whole story: the host is one you allow, over a protocol you do not. Nothing else in the report is unusual, which is what makes this class easy to triage.
  • The issue title says cdn.example.com with no scheme, because grouping is by directive and host. Open the issue and read the blocked URIs on the reports inside it — that is where http:// versus https:// shows up.
  • document-uri tells you where to edit. A handful of product pages is a content problem; every page on the site is a template or a stylesheet.
  • Report-only is also what makes this visible at all for an image. A report-only policy is evaluated against the URL as the page wrote it; the browser's own mixed-content upgrade runs after that, and an enforcing policy sees the upgraded request. The report-only stage of a rollout is where these surface, so read the list before you enforce rather than after.
  • A policy carrying the legacy block-all-mixed-content directive reports under that name instead, which is a useful signal in itself: it means the policy still has the old directive and upgrade-insecure-requests is absent, or the old one would have been inert.
  • A page with no visitors sends nothing. Reports track traffic, so the quiet corners of a site surface last.

Field by field: the img-src reference.

What HeaderHawk does with it

One bad URL in a shared template is thousands of reports a day and one row. The issue carries its report count, the number of distinct pages it appeared on, and when it was first and last seen — and for this class the last-seen timestamp is the satisfying one, because after you fix the URL it stops moving.

Opening the issue lists the individual reports: the exact pages, the full blocked URI with its scheme, the source file where the browser attributed the request, and the policy in force. For an image in a content field the page list is usually enough to find every copy.

Extension and crawler traffic is classified as noise and held out of alerting, which matters more than it sounds on a public site — a reporting endpoint with no filtering is mostly other people's browser extensions, and mixed-content reports from those are somebody else's problem entirely.

This is also the cheapest thing to try. The Free plan covers 3 sites and 10,000 reports a month with no card, which is enough to point a policy at one site and find out what it has.

Where CSP alone is not enough

The honest summary of this page: CSP reporting finds the URLs your own policy objects to. It is not an audit of your site's transport security.

  • The browser's own mixed-content blocking produces no report. Browsers refuse insecure scripts, frames and network calls on a secure page whether or not you have a CSP, and that refusal consults no directive, so nothing is sent. Your reports show what your policy refused, which is a different and smaller set.
  • upgrade-insecure-requests reports nothing, by design. It rewrites the request before it is made, so there is never a violation to report. It is the right fix for a large legacy codebase and it makes the problem invisible here at the same time — that is a trade, and worth making knowingly.
  • The issue title groups by host, not by scheme. http://cdn.example.com and https://cdn.example.com share a host, so they share one issue. The scheme is on the individual reports inside it, and that is where you confirm you are looking at a mixed-content problem rather than a host you forgot to allow.
  • A page nobody visits reports nothing. Every row comes from a real browser loading a real page, so a neglected corner of the site stays invisible until someone goes there. This is a live-traffic signal, not a scan.
  • It says nothing about navigations. A link to an http:// URL is not a subresource, and no directive covers it; upgrade-insecure-requests deliberately leaves third-party navigations alone too.
  • There is no response header monitoring, so nothing here can tell you whether you are serving Strict-Transport-Security, or whether your policy is still being sent at all.
  • Report-only mode prevents nothing, and the fix is not here anyway: it is a URL in your markup, your CMS, or your CDN's rewrite rules.

Where to go next

  • upgrade-insecure-requests is the pragmatic fix for a codebase with too many URLs to find — and the reference explains why it emits no reports.
  • block-all-mixed-content is the deprecated directive you may still be carrying, and what to replace it with.
  • img-src covers the directive in the example above, including the long tail of hosts that user-generated content produces.

Start with one site and one header

Mixed content is what a report-only policy finds first, and it is the quickest thing on this site to fix and verify.