All posts

How to fix BitSight Content Security Policy findings

CentralCSP Team ·

Last update:

A BitSight finding about your Content Security Policy (CSP) is fixable, and you can do it without breaking the site. The reliable path is to deploy a strict policy in report-only first, watch what real traffic does, tighten the policy from that evidence, then switch it to enforce. This post walks through each phase, why it works, and how to map each BitSight remediation hint to a concrete change in your header.

CentralCSP also publishes a tool-driven walkthrough of the same fix, which leans on the scanner and builder to do the work. This post is the companion to that one and goes deeper on the mechanics: what a violation report actually contains, how to read it, and how to convert an unsafe policy into a strict one.

What BitSight is flagging and why it changed

BitSight rescored web application security in 2025. The Web Application Security (WAS) risk vector replaced the older Web Application Headers vector in the headline rating, going live in July 2025 after a preview that opened on April 8, 2025, and it carries the same 5% weight. WAS runs 21 assessments across 5 categories, and one of them is named "Content Security Policy (CSP) Violations".

The scan is non-intrusive. BitSight loads your domain in a headless browser, captures the full page response, and inspects security measures such as Subresource Integrity (SRI) and CSP to check that no untrusted remote resource is pulled in. It does not send crafted requests to trigger or exploit a vulnerability; it reads what the page and its headers actually do.

A few things follow from that:

  • BitSight reads your response headers as a browser would. Raters score the enforced header on your live site, so run an enforced Content-Security-Policy header, not report-only alone, to move the grade. Report-only is the tool for building the policy safely; the enforced header is what clears the finding.
  • The findings are a scoring heuristic on the header string, not a CSP validity test. A policy can be spec-valid and still flagged, and a flagged pattern is something to remove because it weakens the policy, not because it is invalid.

What the CSP findings usually cover

BitSight's CSP-related findings tend to fall into four buckets:

  • A missing Content Security Policy on the assessed host.
  • A weak configuration that uses unsafe keywords such as 'unsafe-inline' or 'unsafe-eval'.
  • CSP violations, where legitimate resources are blocked by the policy.
  • An overly permissive policy that uses a wildcard or allows insecure schemes.

The remediation hints BitSight surfaces line up with strict-CSP best practice. Paraphrased, they amount to: remove 'unsafe-eval', remove 'unsafe-inline', remove blob: and data: from source lists, and remove any lone asterisk (*).

Why fixing CSP may not move your grade right away

Each finding gets a word grade by severity, BAD, WARN, and FAIR among the grades used, and those roll up into a cumulative A to F grade. The most severe findings weigh heaviest, so a higher-severity finding elsewhere in WAS can hold your grade down until it clears.

So if your CSP finding is graded FAIR while a WARN or BAD finding sits above it, fixing the policy alone may not lift the grade until the higher-severity findings clear. Fix the CSP because it genuinely hardens the page, and set expectations that the score may move only once the more severe items are resolved.

The fix workflow

The remediation has five phases: deploy strict in report-only, add a reporting endpoint, collect violation reports, tighten, then enforce.

report-only  ->  collect reports  ->  tighten  ->  enforce

1. Deploy a strict policy in report-only

Start with the policy you want to end up with, but send it on the Content-Security-Policy-Report-Only header. The browser parses the policy and reports what it would block, but blocks nothing, so you can roll a strict policy onto live traffic without risking a broken page. Report-only is an HTTP response header only; it is not available through a <meta> element.

A strict starting point looks like this:

Content-Security-Policy-Report-Only:
    default-src 'self';
    script-src 'self';
    style-src 'self';
    img-src 'self';
    font-src 'self';
    connect-src 'self';
    object-src 'none';
    base-uri 'none';
    form-action 'self';
    frame-ancestors 'none';
    report-to csp-endpoint

2. Point reports at an endpoint

Reports only help if they are delivered somewhere. Declare a reporting endpoint with the Reporting-Endpoints response header, then reference its name from the report-to directive in the policy above. Reporting-Endpoints is the current standard and is broadly available. The older Report-To header is deprecated, and the report-uri directive is deprecated but still honored, so it is worth including as a fallback. Send both report-to and report-uri for the widest coverage, as covered in report-uri vs report-to. The endpoint URL must be HTTPS; a non-secure endpoint is ignored.

Reporting-Endpoints: csp-endpoint="https://<Endpoint-ID>.report.centralcsp.com"
Content-Security-Policy-Report-Only:
    default-src 'self';
    object-src 'none';
    base-uri 'none';
    report-to csp-endpoint;
    report-uri https://<Endpoint-ID>.report.centralcsp.com

You can stand up a reporting endpoint and a dashboard for the reports with CentralCSP, which ingests CSP violation reports and turns them into a script inventory and per-directive view instead of raw JSON.

3. Read the violation reports

Modern CSP violation reports arrive through the Reporting API as a JSON array, POSTed with the content type application/reports+json. The legacy report-uri path posts a single object with type application/csp-report. The fields you care about when deciding what to allow are listed in full in the CSP violation report fields reference:

{
  "type": "csp-violation",
  "url": "https://api-next.centralcsp.com/checkout",
  "body": {
    "documentURL": "https://api-next.centralcsp.com/checkout",
    "blockedURL": "https://cdn.thirdparty.example/widget.js",
    "effectiveDirective": "script-src",
    "disposition": "report",
    "originalPolicy": "default-src 'self'; object-src 'none'; ...",
    "sourceFile": "https://api-next.centralcsp.com/checkout",
    "sample": "",
    "lineNumber": 42,
    "columnNumber": 9
  }
}

Read effectiveDirective to see which directive blocked the resource, and blockedURL to see what was blocked. disposition is report while you are in report-only and becomes enforce once you switch headers, which is a quick way to confirm which header produced a given report. For inline script and style violations, sample and the line and column numbers point you at the exact code to fix.

Work through the reports and sort each blocked resource into one of two piles: legitimate (something the page really needs) or unwanted (an injected or stale resource you do not want). You allow the first pile and leave the second blocked. Collect long enough to capture real usage across your traffic before you trust the list.

4. Tighten the policy

This is where you remove the patterns BitSight flags, using the evidence from the reports. Map each hint to a concrete edit.

Remove 'unsafe-inline' from scripts. Inline scripts and styles are a primary XSS vector, which is why the keyword is flagged. The fix is to give each legitimate inline script a nonce or a hash. When script-src contains a nonce, a hash, or 'strict-dynamic', the browser ignores 'unsafe-inline' entirely, so leaving it in does nothing for modern browsers and only weakens the policy for older ones. That suppression is the mechanism that lets you drop the keyword safely.

Externalize inline event handlers where you can:

<!-- Before: inline handler needs 'unsafe-inline' -->
<button id="buy">Buy</button>
<script>
  document.getElementById('buy').onclick = function () {
    startCheckout();
  };
</script>
<!-- After: external script, no inline handler -->
<button id="buy">Buy</button>
<script src="/js/checkout.js" nonce="r4nd0mN0nce"></script>
// /js/checkout.js
document.getElementById('buy').addEventListener('click', function () {
  startCheckout();
});

For inline scripts you cannot externalize, attach a nonce. The nonce must be at least 128 bits of entropy, base64, generated by a cryptographically secure random number generator, and fresh and unique on every response. Never reuse a nonce across requests.

Adopt the canonical strict policy. The target shape recommended for a strict CSP is nonce-based or hash-based, with 'strict-dynamic':

Content-Security-Policy:
    script-src 'nonce-{RANDOM}' 'strict-dynamic';
    object-src 'none';
    base-uri 'none'
Content-Security-Policy:
    script-src 'sha256-{HASH}' 'strict-dynamic';
    object-src 'none';
    base-uri 'none'

'strict-dynamic' tells the browser to trust scripts loaded by an already-trusted (nonced or hashed) script, and to ignore host allowlists, 'self', and scheme sources for script-src. If your current policy leans on a long list of allowed script hosts, understand that 'strict-dynamic' makes that list irrelevant for scripts, which is usually what you want.

Remove wildcards and insecure schemes. A lone * and an http: scheme source defeat the point of the policy. Replace * with the specific hosts the reports show you actually load. Replace data: and blob: sources with hashes or specific origins; an attacker who can inject a data: URL into a directive that allows it can often bypass the policy. Only keep a scheme source where a report proves a legitimate resource needs it and no tighter alternative exists.

Remove 'unsafe-eval'. This keyword re-enables string-to-code execution (eval, new Function, and similar) across the page. If a library needs it, prefer a build that avoids eval or move that code off the page rather than reopening the policy.

For more on why the inline keyword is worth removing and how to do it cleanly, see the case against unsafe-inline, and for assembling the whole policy from scratch, how to build a strong CSP.

5. Enforce

When the report-only stream is quiet, that is, when the only remaining reports are for resources you have deliberately chosen not to allow, move the same validated policy from Content-Security-Policy-Report-Only to the enforcing Content-Security-Policy header.

Content-Security-Policy:
    script-src 'nonce-{RANDOM}' 'strict-dynamic';
    style-src 'self';
    img-src 'self';
    connect-src 'self';
    object-src 'none';
    base-uri 'none';
    form-action 'self';
    frame-ancestors 'none';
    report-to csp-endpoint;
    report-uri https://<Endpoint-ID>.report.centralcsp.com

Keep reporting on after you enforce. The disposition field now reads enforce, so any new report means a real block in production, which is your early warning that a change broke something or that someone is probing the page.

One caution: a page can pass functionally in report-only and still break under enforcement, because report-only never actually blocks anything. Do not skip the step of reaching zero legitimate violations before you flip the header.

A note on multiple hosts

Scanners assess per host. If your estate spans several subdomains or origins, each host needs its own enforcing policy and its own reporting endpoint binding. A header on www does nothing for app or checkout.

Where this leaves you

Fixing a BitSight CSP finding is a sequence, not a single switch: deploy strict in report-only, collect real violation reports, tighten from that evidence by replacing unsafe keywords and wildcards with nonces or hashes, then enforce. The same enforcing header that clears the finding is also genuinely better protection against XSS and resource injection.

If raw report JSON is more than you want to triage by hand, start a free CentralCSP account to collect the reports, see them grouped by directive and resource, and track the policy over time as you tighten it.

The same enforcing header also helps fix the same findings on SecurityScorecard and improve your overall security headers grade.

Sources