Adding “Content-Security-Policy: default-src 'self'” to a website may immediately disable remote fonts, images, and inline scripts. Security policies should focus on restricting access to real resources, rather than simply using a seemingly strict string. It’s important to understand what a page is actually doing before deciding which actions should be allowed.
This article focuses on websites primarily composed of static HTML. While CSP can mitigate the impact of certain injection attacks, it cannot fix insecure data concatenation, nor can it replace output encoding, permission validation, or dependency review.
Creating a Real Resource Inventory Using Report-Only
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Report-only mode does not block resources, but the browser records loads that violate the policy. Receiving reports requires appropriate endpoints and reporting directives, with browser-support differences taken into account. Setting the header above does not automatically create a server-side report store. During development, inspect the console first, then establish controlled collection.
Reports may contain visited URLs and resource addresses, so limit their collection scope and retention period. Do not turn the reporting endpoint into unlimited storage for arbitrary JSON. Aggregate repeated noise and distinguish browser-extension injections from the site’s own behavior. Otherwise, violations caused by extensions may lead you to weaken the site’s policy unnecessarily.
Assume the website uses scripts from the same origin, a font service, an image CDN, and an analytics interface. Each resource type, loading page, business purpose, and responsible person should be registered separately, rather than just maintaining a list of domain names. Font style sheets and actual font files may come from different sources; this means that a domain name for styles does not automatically allow font files. The analytics script can be loaded, but this does not mean that its network reporting can be accessed via connect-src. Each type of request has different responsibilities, and the policies should also be clearly defined for each.
The resource graph can also reveal the expansion of third-party dependencies. A video component might further load images, subframes, and scripts, and a plugin upgrade might introduce new domains. Allowing every HTTPS source merely to make the component work weakens CSP’s constraints. Decide which functions must remain and which can become ordinary links or same-origin static content. Isolated pages can host the few interactions that need third-party capabilities, without granting the same broad permissions to every article.
The sources cited in the report are not necessarily reliable. Browser extensions, malicious input, and broken resources can all lead to violations. Automatically expanding the allowed list based on logs can turn attack attempts into successful permission requests. The strategy should be to categorize by source, verify that the loading is expected, and then modify the strategy through code review. For unknown sources, first reproduce the loading chain; you cannot simply determine legitimacy based on frequency of occurrence.
Resource permissions should be separated by purpose.
img-src can permit a specific image CDN, connect-src restricts the endpoints scripts can contact, and frame-src restricts pages embedded by this site. Conversely, frame-ancestors controls which ancestors may embed this site. It must be delivered in a response header; an ordinary HTML meta tag cannot provide that capability.
If a website needs to be embedded by a specific partner, frame-ancestors 'none' is not suitable. The strategy is not that the shorter it is, the safer it is, but rather that it should be as close as possible to the business boundary and easily verifiable. Payment, login, and video components may involve navigation between different domains, and should be checked using real-world processes, not just verifying the homepage.
When a supplier’s documentation requires the inclusion of a specific script domain, it should record the corresponding component, applicable pages, and alternative solutions. Any changes to the domain name by the supplier should be treated as a dependency change review, rather than unconditionally adding a wildcard in production environments. If a component requires the ability to include any inline script, it should first be verified whether there is a version without inline scripts, a hashing scheme, or an isolated page. The choice of functionality itself can also narrow down security permissions; not all compatibility issues need to be resolved by loosening security policies.
For pages that need to be embedded elsewhere, configure allowed ancestor origins separately while continuing to prohibit other sites from embedding ordinary articles. Pages with user-uploaded images should serve those resources from controlled addresses; that requirement does not justify allowing arbitrary executable sources. Limit permissions by purpose and page scope so a special feature does not unnecessarily expand the attack surface of the entire site.
Policy maintainers also need to be aware of the limitations of security testing. Observing a sample being blocked in a browser only demonstrates the sample’s behavior under that specific policy and browser, and does not cover all possible injection forms. While placing a sample in an automated test is valuable, it is still necessary to review the output concatenation and dependency loading. When a new violation is identified, first assess whether its requirements are reasonable, and then modify the policy, rather than treating “no report” as the sole standard for deployment.
Inline code requires a clear handling method
Static pages have fixed inline scripts that can be identified using a hash. If the script bytes change, the hash must be updated. Changes in whitespace can also affect the match. When building tools compress or inject scripts, the hash should be calculated for the final output, not guessed from the source file.
Dynamic pages often use a nonce generated for each response, and simultaneously write it to both the response header and the allowed script tags. It’s not advisable to hardcode a fixed nonce into static HTML for long-term reuse, as this wouldn’t provide the expected isolation. For purely static sites, moving a small amount of inline logic to an externally hosted script from the same origin can often be easier to maintain, but it’s still important to prevent attackers from controlling executable files on the allowed source.
'unsafe-inline' significantly relaxes restrictions and should not remain indefinitely because of a single compatibility issue. Inline styles and scripts are governed by different directives; fixing style loading must not also open up script execution. Review each policy requirement from third-party components individually rather than copying every wildcard domain into the policy.
For a fixed theme-initialization script, the build tool may change whitespace or insert attributes. Generate the hash from the script content’s bytes and include it in the corresponding response policy. Produce the HTML and header file in the same build so a script update does not leave the policy referring to an old hash. Development servers also load hot-reload scripts, so a development policy is not evidence that production passes acceptance checks.
If different pages contain different inline scripts, the strategy can be generated on a per-page basis, or a limited hash set can be used. A global set is easier to deploy, but it increases the amount of content that can be executed on each page. A per-page strategy is more precise, but it increases the number of output rules. Personal static sites can often move necessary logic to external files from the same origin, reducing the burden of synchronization and maintenance. Regardless of the chosen method, it is important to check whether content that can be uploaded from the allowed source could be interpreted as a script.
The dynamic nonce must be unpredictable and consistent with the current response. If the cached HTML and response headers come from different versions, a mismatch in the nonce will disrupt normal functionality. Similarly, fixing the nonce for caching purposes eliminates the intended security guarantees. A complete design is required for dynamic pages, edge caching, and nonces, and simply inserting a nonce generation function into a static project does not constitute adequate protection.
A Later CSP Header Does Not Override an Earlier Policy
When a response contains multiple CSP policies, the browser applies the applicable policies together. A more permissive new policy does not cancel an older one. Applications, proxies, and hosting platforms may each set headers, leaving a resource permitted by one policy but blocked by another. Inspect actual response headers during acceptance testing, not just configuration files in the source tree. Report-only and enforced policies also coexist; receiving a report does not establish that a resource was blocked.
Some directives do not inherit the default behavior of default-src, and you must consult the specifications and documentation for each directive. Do not assume that all navigation, forms, and embedded content are restricted simply because “default-only” is used. The target for form submission and the target for script connection are distinct capabilities that must be configured based on actual business requirements. Pay particular attention to testing the redirects after user interaction in login and payment processes, rather than just the initial resource loading on the page.
Meta-delivered policies have functional limitations, and document-parsing timing affects their coverage. Send headers at the deployment layer when response-level capabilities such as frame-ancestors are required. Explicitly define whether policies cover service workers, error pages, and redirect responses too; do not add headers only to successful HTML responses. Protection depends on what the browser actually receives, not a checkbox in an administration panel.
Report endpoints require their own security and privacy design
The report may contain the complete page URL, which could create sensitive logs when used in queries with tokens or user input. It is important to cleanse unnecessary parameters, limit field lengths, and request sizes on the server-side, and to set a retention period. The endpoint should accept input from the browser and should not trust the declared page source. It should also prevent the report from directly triggering external requests. Malicious clients can generate a large number of reports, and rate limiting is a basic requirement.
Aggregate by directive, page type, code version, and browser so repeated reports of one extension injection are not mistaken for a site regression. Report counts need a denominator, such as covered page visits, rather than only an absolute total. Browser-support differences and report-delivery conditions make collection incomplete; no reports does not prove there were no violations. Combine logs with active testing rather than waiting for production users to perform the team’s tests.
For private pages, aim to minimize the reporting content, and only retain categories and versions when necessary. The retention policies of external analysis platforms also need to be confirmed. Security monitoring should not create new vulnerabilities due to excessive context collection. Even if the reporting mode does not interfere with user operations, endpoint failures and logging costs can still impact the system. Resource limit testing should be performed before deployment.
Treat Build Artifacts as Policy Inputs
A single build upgrade might combine external scripts into inline blocks, or transform previously inline initialization logic into dynamic imports. The resource list in the source code remains unchanged, but the actual execution graph in the browser changes. Policy review should be based on the final HTML and script entry points, rather than solely on searching for <script> tags in the source code. The build report can list inline blocks, external sources, and dynamic loading dependencies, and pass the differences to the maintainer for confirmation.
For fixed-script hashes, the generator reads the actual script text from the final artifact, produces the policy, and verifies that the generated HTML and policy correspond. Do not reformat the script before calculating its hash: the browser does not compare a reformatted review copy. Cached header files and HTML should belong to the same version; deploying only one can block legitimate scripts. Change one byte of script text in a test and confirm that the old policy no longer permits it, while the new policy allows it to run.
External scripts are not inherently risk-free, even if they originate from the same domain. If a user-controlled upload path exists within the same domain, or if there are interfaces that allow the execution of JavaScript, the actual permissions of the entire self can be significantly greater than what appears to be the case. It is crucial to isolate the upload source, set the correct content type, and review any executable entry points. CSP and upload isolation work together, and neither can guarantee that a single origin can safely handle any user-provided content and then allow that origin to execute arbitrary code.
Dynamic loading chains also need a clear owner. When a trusted script creates further scripts, the relevant CSP capabilities depend on the actual policy and browser support. For example, strict dynamic-trust mechanisms can change the role of host allowlists. Do not pile on advanced directives without a corresponding requirement. When they are needed, explain their effects using official specifications and browser tests. A policy string copied from a vendor is still security configuration that the maintainer must understand.
Writing a Capability Statement for Third-Party Components
Suppose a video component requests four categories of sources: scripts, frames, images, and connections. Its capability request should list the purpose, page scope, login requirements, fallback entry point, and removal method for each category. Review whether those capabilities fit the site’s task, not how famous the vendor is. If an article only needs a video link, the benefits of a complete embed may not justify maintaining four categories of long-term permissions.
After a third-party dependency upgrade, compare its request graph again, especially new connection origins and broader inline requirements. In a preview environment, block new origins to observe which component functions fail. Do not use that experiment as justification for retaining an incomplete, misleading control. Either grant explicit capabilities and verify them, or provide a reliable alternative. Restricting permissions while preserving a way to complete the task is better than displaying a component that cannot play the video.
Removing components should also remove any unused permission grants, and maintain a negative sample demonstrating that the source cannot continue to be used. Permission accumulation is a long-term maintenance issue, and only adding without removing will gradually erode the effectiveness of the policy. For sites that share policies across multiple pages, first verify whether other pages still depend on the source. More granular page-level policies can reduce permissions, but they also increase the complexity of configuration and deployment. The decision should be documented in resource records, rather than being determined by a single on-line failure.
Using Report Analysis to Prevent Errors from Expanding Allowed Lists
When many blocked-uri events arrive, aggregate them by directive, page type, and version, then sample the corresponding loading chains. Extension injections often involve extension-specific origins; attack inputs may involve unfamiliar remote domains; a legitimate site regression may cluster around a new release and a particular component. Frequency is a classification clue, not authorization evidence. An attacker can manufacture high report counts, so counts must never trigger automatic allowlist updates.
The report content is not a browser authentication credential. Endpoints can be directly accessed by any client, and fields should be validated for size, type, and allowed formats. Do not proactively download resources based on URLs in the report; this would turn the log receiver into a server-side request entry point. Manual review requires using a controlled environment and security tools when accessing external addresses, while maintaining separate responsibilities for data collection and investigation.
When comparing violation counts, estimate sampling coverage and the page-visit denominator. Fewer reports may reflect a policy fix, an endpoint failure, unsupported browsers, or changed sampling. If an active test finds the endpoint unreachable, raise a separate alert instead of treating zero reports as a security victory. Delete unnecessary URLs and sample text when the retention period expires. Aggregate trends can be retained longer without retaining user input indefinitely.
Actions to Take When Enforcement Fails
If the theme button is malfunctioning, first verify whether the script hash is incorrect, whether there is a conflict in the actual header combination, or whether the resources are missing. Each of these causes requires a different fix: resynchronize the output, remove the incorrect duplicate configuration, or restore the script file. Directly adding unsafe-inline will turn a deployment issue into a long-term permission relaxation. Speed of recovery is important, but it should be restored to the previously verified configuration, and then the issue should be reproduced in preview before making corrections.
If the login or payment process is interrupted, the affected entry should be paused and the actual status should be provided, rather than repeatedly prompting the user. CSP typically restricts loading and execution, without providing mechanisms to undo business-side effects; a request may have been successfully submitted, but subsequent pages may be inaccessible. The recovery interface needs to query the actual business result to avoid treating visual failures as transaction failures. The failure of the security header should be treated separately from the unknown business result, which can reduce the risk of repeated payments or repeated operations.
The acceptance report records the supported browsers, the actual implemented policies, the results of positive tasks, and the negative blocking samples. For unverified platforms, the fields should be left blank, and the assumption that all standard document support is guaranteed should not be made. Completing the policy deployment means that the necessary functions within the verified range are available, and the ability to escalate privileges is limited. This does not mean that all injection risks have been eliminated.
Enforce a rule from a single page
First, run the report mode to cover the main browsers and page types. Then, enable enforcement policies for simple pages. Check navigation, search, images, fonts, analytics scripts, and error pages. The generated HTML may differ from the development environment, so it is necessary to test in a deployment preview environment.
When deploying, preserve the rollback path. When a violation is detected, it’s necessary to determine whether it’s due to code regression, changes made by third parties, or an attack attempt. The system should not automatically add the source of the violation to the allowed list. To improve CI, add checks for changes in resource domain names and inline scripts. This allows security policies and page modifications to be reviewed simultaneously.
The acceptance result should be “necessary functionality works and unauthorized scripts are blocked,” not “the console has no warnings.” CSP derives its value from a continuously maintained resource contract. Broadening the policy to allow every source and then claiming that protection is enabled provides no meaningful constraint.
Furthermore, it’s important to distinguish between inline elements and inline attributes. Allowing a fixed hash for a <script> element does not mean that the onclick on a button also receives the same permission. If the component library still generates event attributes, prioritize migrating event bindings to approved scripts, rather than expanding inline execution permissions for all buttons to maintain compatibility with a small number of older buttons. Test both the normal binding version and the synthetic event attribute version of the same button, and record user actions and violation categories separately.
There are also similar distinctions in styling: using a fixed style element and setting the style attribute at runtime are two different approaches. Browsers that support the corresponding CSP directives can express more granular permissions by specifying elements and attributes, but the project must verify the fallback rules of the target browser and the specific directive. One approach uses CSS classes to achieve styling, while another uses repeatedly writing to the style attribute to achieve layout. We cannot expect the same policies to apply simply because they both use the term “style.”
This check determines the source of the attributes: are they generated by the author’s Markdown, component templates, or third-party initialization scripts? First, identify the source, and then decide whether to modify or authorize the output. If only certain chart pages require specific functionality, record the page scope and alternative solutions, while maintaining the original constraints for regular articles. The test results only indicate that the required output formats have been covered, and do not imply that all dynamic styles are compatible.
References: MDN CSP, CSP Level 3.