Content Security Policy for Browser Application Owners
Plan, stage, and maintain a Content Security Policy with clear ownership, reporting evidence, compatibility checks, and rollback boundaries.
BotBrowser Team
Want the structured docs for Deployment?
This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.
Content Security Policy (CSP) is an application response policy that tells a supporting browser which kinds of resources a document may load or execute. It can reduce the impact of some injection mistakes by constraining scripts, connections, frames, workers, and other resource types. It is not a complete application security program. Server authorization, output encoding, dependency review, session design, transport security, and incident response remain separate responsibilities. The W3C CSP Level 3 specification defines the policy model, while MDN's CSP guide explains practical deployment choices.
Treat CSP as a product and deployment decision. The application team owns the resources that the page intentionally uses, the infrastructure team owns how the response is delivered, and each vendor or platform owner owns the endpoint they operate. A browser violation report is an observation about a policy and a request. It does not prove that an attack occurred, that a missing report means a clean application, or that the business operation completed. Keeping those evidence boundaries visible prevents a useful browser control from being mistaken for a universal guarantee.
Use this article to plan policy design, ownership, report-only rollout, deterministic browser checks, compatibility, and rollback. The examples use synthetic pages and bounded receipts. They do not provide a way to weaken a policy to load untrusted content, collect credentials, defeat a browser control, or alter a service that the application team does not own. For adjacent response-header responsibilities, see the custom HTTP headers guide, and for release evidence see the browser release validation guide.
Define the policy purpose and boundary
Begin with the user journeys that the policy must protect and the resources each journey needs. Inventory scripts, styles, fonts, images, media, frames, workers, WebSockets, fetch destinations, form targets, and navigation targets. Include resources introduced by tag managers, payment widgets, analytics, support tools, and service workers. An inventory is an ownership document, not a list copied from a browser log. Name the business purpose, resource owner, data sensitivity, environment, and expected lifetime for every entry.
Separate the browser enforcement boundary from the application's trust boundary. A script-src decision can restrict a script request, but it cannot decide whether an API call is authorized or whether the script itself has a server-side privilege. A connect-src decision can constrain browser connections, but it does not replace access control at the endpoint. A frame directive can limit where a document embeds or is embedded, but it does not verify the code or policy of the child document. Write these distinctions into the security record before choosing directive values.
Use the narrowest policy that matches an owned application contract. Prefer explicit origins and paths where they are stable, and avoid broad wildcards when a smaller set is practical. Be careful with schemes, ports, redirects, and subdomains: a source expression that looks first-party can include more hosts than the service owner intended. Treat a third-party origin as a dependency with a named owner and renewal path. If the team cannot explain why an origin is needed, do not add it merely to silence a violation.
Decide what success means for each journey. A useful result might be “the application shell renders, the approved module loads, and an unapproved test resource is blocked.” It is not “the browser showed no reports, therefore the application is secure.” Record the expected visible state, the policy header delivered to the final document, and any application-side evidence needed to establish completion. Keep secrets, customer content, full request bodies, and unrelated browser attributes out of policy receipts.
| Policy question | Evidence to inspect | Owner and decision |
|---|---|---|
| Which executable code is intentional? | Final script URLs, inline code inventory, deployment artifact | Application owner approves a nonce, digest, or explicit source |
| Which network destinations are required? | fetch, WebSocket, worker, and form targets in an approved journey | Application and service owners document exact origins |
| Which embedded documents are trusted? | Frame URLs, redirects, and child ownership | Product owner approves the relationship and fallback |
| What should be reported? | Delivered report-to or report-uri configuration and retention rules | Security owner sets privacy and operational limits |
Choose directives without weakening trust
The policy should express a deliberate trust model rather than a collection of fixes. default-src provides a baseline for resource types that do not have a more specific directive. Specific directives such as script-src, style-src, img-src, font-src, connect-src, frame-src, worker-src, media-src, object-src, base-uri, form-action, and frame-ancestors describe different browser decisions. The exact support and fallback behavior varies by browser release, so validate the policy in the browsers and application versions that the product supports.
For executable code, prefer a design that makes approved code intentional. Nonces are per-response values that the server places on approved inline elements and in the policy. A digest binds a known inline block to an exact integrity value. External scripts still need an allowed source and should be controlled through dependency ownership and integrity practices. Avoid treating a nonce as a password or putting a reusable value in a cache shared by unrelated responses. A nonce authorizes the element for that response; it does not authorize an API, a user, or a vendor account.
Inline style and script allowances deserve a review with the application team. A broad allowance can make a policy easier to deploy while reducing the protection it provides. If a legacy framework requires inline behavior, document the concrete dependency, test the migration path, and keep the exception as narrow and temporary as the owners can support. Do not add unsafe-eval, unsafe-inline, wildcard sources, or an entire vendor domain just because one violation is inconvenient. A compatibility exception should have a reason, owner, expiry or review date, and an observable fallback.
Other directives protect different boundaries. base-uri prevents an unexpected base URL from changing relative resource resolution. form-action limits where forms submit. frame-ancestors controls which pages may embed the document and is distinct from a child frame's frame-src permission. object-src 'none' is a common hardening choice when legacy plugins are not required. upgrade-insecure-requests can change URL handling, but it does not repair every mixed-content or third-party integration issue. Record the expected effect before enabling each directive.
Do not confuse CSP with related headers. Permissions Policy controls feature delegation, COOP and COEP shape browsing-context and embedding relationships, and transport headers address different threats. A CSP can complement those controls, but it cannot supply their guarantees. The secure-context guide explains why a browser security state is also separate from application policy. Keep one owner and one acceptance test for each header so a change in one control does not silently change the claim made by another.
Stage with Report-Only evidence
Start with Content-Security-Policy-Report-Only on a staging or controlled production route. Report-Only lets a supporting browser evaluate a candidate policy and send violation observations without blocking the resource. It is valuable for discovering missed dependencies, but it is not a security pass. A report may be absent because a browser does not support the reporting path, a route was not exercised, a report was dropped, or the resource was handled by another document. Treat absence as “not observed,” not “allowed and safe.”
Give every candidate policy a version and an owner. Store the policy text, the route or template that emits it, the browser support range, the test journey, and the date of review. Normalize reports into a small record containing the document origin, blocked URI classification, effective directive, disposition, browser family, and scenario. Remove query strings, tokens, cookies, page text, and other sensitive fields before retention. A report collector should be an approved service with access controls and a documented retention period, not an ad hoc endpoint that accepts arbitrary data.
Triage each report against the inventory. A blocked URI can be an intentional denial, a missing same-origin asset, a vendor dependency, a redirect to an unexpected host, or a browser-generated value that is not an application resource. Verify the final request and response in the approved environment. Do not automatically add the reported origin. Ask who owns it, what data it receives, whether it supports the required policy, and what user-visible outcome exists if it remains blocked. A report proves that the browser evaluated a candidate policy; it does not prove that the origin is trustworthy.
Run positive and negative cases. The positive journey loads every resource that the product promises. The negative fixture asks the page to use one synthetic resource that the policy intentionally disallows and asserts a visible, bounded fallback. Exercise both the header and the application behavior so a green result cannot be caused by a page that never attempted the request. Keep the fixture origin and response under the test team's control, and retain only a short outcome and cleanup status.
| Rollout stage | Browser response | Evidence to retain | Promotion rule |
|---|---|---|---|
| Inventory | No candidate policy yet | Resource ownership record and journey list | Every required dependency has an owner |
| Report-Only | Violations are reported, resources still load | Versioned policy, redacted reports, positive and negative results | Each report is classified or accepted as an explicit exception |
| Enforced in staging | Violations can block resources | Final response header, visible result, compatibility matrix | Core journeys pass in supported browsers |
| Enforced in production | Policy blocks outside the contract | Deployment receipt, monitoring, rollback artifact | Release owner accepts residual compatibility risk |
Run a deterministic blocked-resource fixture
The fixture below uses an owned test page and an intentionally blocked image. It verifies a visible fallback, records no private request data, and removes the temporary node even when an assertion fails. Serve the page through the same response-header path as the candidate application. Configure the expected result in the test case rather than deriving it from the observed result. The fixture is a browser observation; it does not prove that a server rejected a request, that an attack was prevented, or that a user completed a business operation.
async function checkCspFallback(page, expectedBlocked) {
const result = await page.evaluate(async expected => {
const host = document.createElement('section');
const image = document.createElement('img');
const status = document.createElement('output');
let violation = false;
host.dataset.test = 'owned-csp-blocked-image';
status.setAttribute('aria-live', 'polite');
status.textContent = 'Waiting for policy result';
image.alt = 'Owned fixture resource';
const outcome = new Promise(resolve => {
document.addEventListener(
'securitypolicyviolation',
event => {
if (event.blockedURI.endsWith('/fixtures/csp-owned-image.png')) {
violation = true;
resolve('blocked');
}
},
{ once: true }
);
image.addEventListener('load', () => resolve('loaded'), { once: true });
image.addEventListener('error', () => resolve('error'), { once: true });
});
image.src = '/fixtures/csp-owned-image.png';
host.append(image, status);
document.body.append(host);
let timeoutId;
const timeout = new Promise(resolve => {
timeoutId = window.setTimeout(() => resolve('not-observed'), 5000);
});
const observed = await Promise.race([outcome, timeout]);
window.clearTimeout(timeoutId);
const blocked = violation;
status.textContent = blocked
? 'Fallback: image blocked by the application policy'
: observed === 'loaded'
? 'Control: owned fixture loaded'
: 'Fixture failed without a policy violation';
const passed = expected
? observed !== 'error' && observed !== 'not-observed' && violation
: observed === 'loaded' && !violation;
return { marker: host.dataset.test, observed, blocked, passed };
}, expectedBlocked);
try {
if (!result.passed) throw new Error(`Unexpected CSP fixture result: ${result.observed}`);
return result;
} finally {
await page.evaluate(() => document.querySelector('[data-test="owned-csp-blocked-image"]')?.remove());
}
}
Serve /fixtures/csp-owned-image.png from the application-owned test stub with a small valid image response. The control policy allows img-src 'self', so the image must load; the candidate policy uses img-src 'none', so the browser emits securitypolicyviolation and the visible fallback is recorded. If the browser does not emit the expected event, report “not observed” and investigate the policy, browser support, and fixture setup. Do not turn a network or policy error into a pass. A deterministic failure should leave the test context and artifact directory clean.
Run the same fixture once with the control policy and once with the candidate policy, passing false and true respectively. Keep the receipts separate. A candidate result that says “blocked” is useful only when the control run proves that the owned stub was reachable and the candidate run reports a policy violation. Include the final URL, policy version, browser release, marker, visible outcome, and cleanup result. Do not include cookies, authorization headers, full console logs, or a copied production response.
When setup, assertion, and cleanup can fail, preserve the first workflow failure. Report cleanup as a separate field and close the page and context in a finalizer. A retry starts a new context and a new receipt. A later pass does not erase uncertainty about an earlier request, especially when the candidate policy was Report-Only and the resource could have reached a real service. Use an owned synthetic origin for the negative case so retries cannot mutate a remote record.
Validate compatibility and ownership
Test the policy on the browsers, operating systems, document modes, and application routes that the team supports. Include redirects, cached responses, service workers, module scripts, workers, frames, form submissions, and dynamic imports where they exist. A policy present on an edge configuration file is not proof that the browser received it. Inspect the final response after redirects and confirm which document owns the policy. A nested document can have its own policy and its own resource decisions.
Keep the application and infrastructure contracts explicit. The application owner defines required resource behavior and fallback UX. The hosting or CDN owner confirms the header is emitted on every canonical route and is not overwritten by a cache variant. The security owner defines reporting, retention, and incident boundaries. The vendor owner confirms supported origins, redirects, and data handling. A release owner resolves conflicts and records the accepted policy version. If a service is optional, document the visible alternative; if it is essential, do not promote until its contract is compatible.
Watch for policy interactions with caching and templating. A per-response nonce must not be reused across unrelated responses, and a cached HTML document must carry the nonce that matches its own policy. A report endpoint must not expose sensitive query values. A service worker can serve a document or resource from a previous version, so test a clean context and a realistic returning context when the application uses one. Browser storage isolation helps make a journey repeatable, but it does not delete server records or repair a stale deployment.
Review policy changes as code and infrastructure. Diff the directive list, source expressions, nonce or digest generation, response route, report destination, and test receipt schema. Require an owner for each added source. Expire temporary exceptions or schedule a review. Remove a source only after a positive journey confirms the dependency is gone and an approved deployment has removed it. A clean report stream without an exercised journey is not evidence that a dependency was retired.
| Compatibility case | Check | If it fails |
|---|---|---|
| Inline application bootstrap | Nonce or digest matches the delivered response | Fix generation or use an external owned module |
| Module or worker import | Final URL and applicable script or worker directive | Add the exact owned source or ship the supported fallback |
| Frame or form journey | Child origin, redirect chain, frame-src, frame-ancestors, and form-action | Coordinate with the owner; do not add a global wildcard |
| Cached or service-worker response | Policy and resource version match the candidate release | Purge through the approved process or wait for controlled expiry |
Monitor, roll back, and maintain
Promote enforcement in a bounded route or release slice first. Monitor blocked-resource counts by policy version, route, browser family, and dependency classification. Keep the aggregation small enough to avoid retaining user content. A spike can indicate a real dependency change, a cache mismatch, a redirect change, a browser behavior change, or a collector problem. Compare the observation with a known positive journey before changing the policy. Never treat a report volume boundary as a universal security or availability metric.
Prepare rollback before enabling enforcement. Keep the prior response policy, application version, and test receipts available. If an essential workflow breaks, restore the last known-good policy and application path together, then rerun the same positive and negative fixtures. If the code relied on a newly allowed resource, guard it behind the runtime fallback or ship the compatible application version. Rollback should not remove transport protection, authorization checks, or logging needed to understand the failure.
After rollback, preserve the candidate reports and label them as historical. Do not silently delete evidence or present a Report-Only result as an enforced result. Open a bounded follow-up for the resource owner, policy owner, or release owner. The next candidate should change one known condition and repeat the same fixture. This makes it possible to tell whether the fix addressed a missing source, a bad nonce, a redirect, a cache, or an application fallback.
Maintain a policy record after release. Record the current header, route scope, policy version, source owners, report destination, retention rule, supported browser range, known exceptions, and the date for review. Recheck after a framework upgrade, a CDN rule change, a vendor integration change, a new frame or worker, or a change to the canonical host. A CSP is part of the deployed application contract, so stale documentation is itself a maintenance risk.
What BotBrowser can and cannot validate
BotBrowser controlled contexts can run an authorized application journey with a clean or declared browser state, observe whether the browser blocks a synthetic resource, and compare the application's visible fallback across repeatable runs. The BotBrowser multi-account isolation documentation describes isolated BrowserContexts and their browser-managed state boundaries. This capability is useful for validating the browser-visible portion of a CSP acceptance case: final URL, delivered policy metadata that the test is allowed to inspect, blocked or loaded outcome, and cleanup receipt.
BotBrowser does not author or deploy response headers, choose a safe source list for an application, validate server authorization, inspect a dependency's supply chain, or prove the absence of injection vulnerabilities. It cannot make an unowned third-party resource trustworthy, grant a vendor an exception, or guarantee that every browser enforces the same directive behavior. It also cannot prove that an application record was created, a payment was accepted, a server session was revoked, or a security incident did not occur. Those facts require evidence from the application, service, infrastructure, and security owners.
BotBrowser supports authorized browser journeys that observe a blocked-resource result in an isolated context, but it cannot author or deploy the policy, validate server authorization, or prove a security outcome. Keep its receipts narrow: scenario name, policy version, final document URL, browser build, visible result, synthetic route classification, and cleanup status. Do not put customer content, credentials, cookies, or full violation details in the receipt. Label a pass as “browser policy observation” and link the separate application or service evidence when a business claim is needed. A clean BrowserContext is an execution condition, not a server-data deletion mechanism or a security certification.
Before release, have the owners review four non-claims: a report is not proof of an attack; no report is not proof of safety; a blocked resource is not proof that a server rejected or deleted anything; and BotBrowser observation is not a substitute for application authorization and dependency review. Then record the accepted policy, compatibility result, fallback, monitoring owner, and rollback artifact. That record lets the team improve protection without weakening the policy to make an unknown dependency load.
Revisit the same user journeys when the policy or an owned dependency changes. A narrow, versioned comparison makes it easier to identify whether the browser decision, the application's fallback, or a separate service outcome changed.
BotBrowser Team
Sources
Related Articles
Take BotBrowser from research to production
The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.