CSP for SaaS platforms: security questionnaires, logged-in apps and customer trust

Where CSP matters for a B2B SaaS: the enterprise questionnaire that asks about it, the rating vendors scoring your headers, and the logged-in app where a compromised script would hurt most. Why SPAs complicate it, and why crawler-based tools stop at your login page.

Published · Updated

The marketing site gets the security-header attention because it is the part anyone can scan. Then a prospect’s due-diligence questionnaire lands, question 47 asks about Content Security Policy, and the honest answer covers exactly the pages that matter least.

For a B2B SaaS, CSP shows up in three places. Enterprise security questionnaires ask about it explicitly. Prospects’ security teams and rating vendors (BitSight and its peers) score your response headers before anyone signs. And the logged-in application is where a compromised script would actually hurt: session tokens, customer records, admin actions. Treat the app itself as the priority, roll the policy out in report-only mode, and use browser-sourced reporting, because crawler-based tools cannot see past your login page.

Your headers get read before your contract does

Three different audiences look at the same header, and none of them warn you first.

The questionnaire is the explicit one. Somewhere in the vendor-assessment spreadsheet there is a row about browser security controls or XSS mitigations, and “we don’t currently set a CSP” is an awkward cell to fill in when the deal is six figures.

The quiet one is the prospect’s security engineer, who spends thirty seconds scanning your domains before the review call. We have been on both sides of that call. A missing or unsafe-inline-everywhere policy does not kill a deal by itself, but it sets the tone for every question that follows.

The third audience is algorithmic. Security rating platforms score externally observable signals, headers included, and your buyer may be looking at that score before your first meeting. If a BitSight report has already flagged your CSP, there is a dedicated walkthrough for fixing those findings on the main site.

One useful habit: run your own domains through a CSP scanner before the prospect does. CentralCSP’s is free, no account, and returns 0–100 scores for security and quality you can screenshot straight into the questionnaire response instead of writing a paragraph of prose.

Why the SPA makes this harder than the marketing site

A static marketing site takes a CSP in an afternoon: few scripts, known origins, done. The app is a different animal, and the reason is your bundler.

Modern SPAs ship a small bootstrap script that then loads hashed chunks (main.a3f9c1.js and friends) and pulls more via dynamic imports as the user navigates. The file names change on every build. An allowlist can cover the origin, but the moment a chunk loads from a CDN, or the bundler injects an inline runtime snippet, you are either maintaining hashes per build or reaching for unsafe-inline. That last option quietly deletes most of the protection you were trying to buy.

The pattern that works is strict-dynamic with a nonce. The server stamps a nonce on the bootstrap script tag. strict-dynamic then lets that trusted script load whatever chunks it creates, without you enumerating them. Static inline snippets that never change can be hashed instead. The trade-offs between the two are their own topic, covered in nonces vs hashes, but for a chunk-loading SPA the short version is: nonce the entry point, let trust propagate.

Crawlers stop at your login page

Here is the structural problem with most header-scanning tools: they are crawlers. They see what an anonymous visitor sees, which for a SaaS means the marketing site and a login form. The authenticated app, the part holding the session tokens and the customer data, is invisible to them. Scanning the login page and calling the app covered is like auditing a bank by photographing the lobby.

Browser-sourced reporting inverts this. Add a Content-Security-Policy-Report-Only header to the app and every real user’s browser reports violations from the pages it actually renders: the dashboard, the billing page, the admin console, each tenant’s feature-flag combination. Coverage comes from your real traffic, not from what a bot could reach.

And no, this does not leak your customers’ screens. A violation report carries the document URL, the directive that fired and the blocked script’s origin or URL. Not the DOM, not form contents, not anything the user typed. You learn that sketchy-cdn.example tried to load a script on /settings/billing, which is exactly what you want to know, and nothing else.

Rolling it out without breaking the app

The sequence that works, condensed:

  • Ship report-only on the app itself, marketing site included but not the priority. Give it 1 to 4 weeks of representative traffic. How long to stay in report-only has the graduation criteria.
  • Build the policy from what real authenticated sessions reported. CentralCSP’s Builder assembles it directive by directive and shows each proposed source with the evidence behind it (how many browsers, which pages, last seen), which matters when you have to explain the policy to a reviewer later.
  • Turn on the Script Inventory. Browser-sourced, no agent in your app: every script that loads in production, with origin and integrity hash, on every plan. On Scale, the Technologies view identifies the library and exact version behind each script file and matches it against known CVEs. Front-end dependencies drift in ways your lockfile audit never sees, because half of them arrive from tags and third-party widgets rather than node_modules.
  • Keep the scanner scores current and paste them into the next questionnaire.

One last point for the same questionnaire: whatever tool collects your violation reports becomes a subprocessor handling your traffic metadata, and your customers will ask where it runs. CentralCSP processes everything on OVH in France, nothing leaves the EU, with a public DPA, which keeps your own GDPR answer clean. If EU processing is a hard requirement on your side, the EU-hosted CSP monitoring comparison goes through the options.

The prospect’s security team is going to look either way. Better that what they find is a real policy on the pages that matter.

Frequently asked questions

Do enterprise security questionnaires really ask about Content Security Policy?

Regularly, yes. Header hygiene shows up either as an explicit CSP line item or under a broader browser security controls question. And even when the questionnaire skips it, the prospect security team usually runs a header scan on your domains before signing, and rating platforms like BitSight fold externally observable headers into the score your buyer sees. An empty CSP header is visible to all three.

How do I add a CSP to a single-page application?

Allowlists fight your bundler, so use nonces with strict-dynamic instead: the server stamps a nonce on the bootstrap script, and strict-dynamic lets that trusted script load the hashed chunks and dynamic imports it needs without you enumerating them. Ship it as report-only first, watch the violations from real users for a few weeks, then enforce.

Can a CSP scanner see the logged-in part of my app?

No. Crawler-based scanners see your marketing pages and your login screen, then stop. The authenticated app, which is where session tokens and customer data live, is invisible to them. Browser-sourced reporting inverts this: every real user session reports violations from the pages it actually renders, behind the login included.

Do CSP violation reports expose customer data from authenticated pages?

No. A violation report carries the document URL, the directive that fired and the blocked resource origin or script URL. It does not contain the DOM, form values, or anything the customer typed. You learn that an unexpected script tried to load on /billing, not what was on the billing page.