Home / Articles / Web Security
Web Security

CSP for Websites: Making Browsers Help Protect Pages from XSS

Content Security Policy or CSP can serve as an additional layer of defense when a website loads a lot of JavaScript, third-party services, and dynamic content. Here’s how to understand, test, and implement it without causing...

CSP untuk Website: Membuat Browser Ikut Menjaga Halaman dari XSS

When a website is affected by Cross-Site Scripting or XSS, the issue is not just with the input data that gets through. The browser may also execute scripts from sources that should not be trusted. This is where Content Security Policy or CSP comes in handy: a rule from the server that tells the browser which resources are allowed to be loaded and executed.

CSP is not a replacement for input validation, sanitization, or output encoding. However, it can serve as a second layer of defense. If there is a vulnerability that allows foreign scripts to enter the page, a strict CSP policy can limit those scripts from executing easily.

What exactly does CSP do?

Imagine a website as a building with many doors: doors for JavaScript, images, fonts, API connections, iframes, and form submissions. Without rules, the browser tends to follow the instructions embedded in the page. CSP acts like an access list that says, “For this type of resource, only the following sources are allowed.”

CSP rules are typically sent via HTTP response headers like this:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'self';

This example generally means resources can only come from the same domain, scripts can only be loaded from the same domain, old plugins like objects cannot be used, and pages can only be displayed in frames from the same site.

Modern browsers widely support CSP. However, the final outcome still depends on the website structure, the user's browser, and the third-party services used.

Key Parts of a CSP Policy

default-src as the base rule

default-src serves as a fallback rule for resource types that do not have specific rules. The value 'self' means the browser only trusts the same origin, which is the combination of the same protocol, domain, and port.

For a simple website, the following initial policy can be a starting point for discussion:

Content-Security-Policy: default-src 'self';

However, do not assume this rule is suitable for all websites. WordPress, online stores, or applications that use CDNs, Google Fonts, payment gateways, analytics, and chat services usually require an additional list of sources.

script-src to limit JavaScript

This section controls where JavaScript can be loaded from. One of its important goals is to prevent the browser from executing inline scripts or scripts from foreign domains.

script-src 'self';

This rule can cause code like <script>... and event attributes like onclick to not work. From a security perspective, this is good. From a compatibility perspective, older websites may immediately show errors because they still rely on inline code.

Avoid adding 'unsafe-inline' just because some parts of the page are broken. This value can indeed restore compatibility, but it also reduces the protective benefits against XSS. It’s better to move inline JavaScript to separate files or use nonce and hash if the application architecture requires it.

frame-ancestors to prevent clickjacking

Clickjacking occurs when your page is displayed within a frame of another site, and users are tricked into clicking something without realizing it. To prevent the page from being displayed in a frame, use:

frame-ancestors 'none';

If the page needs to be displayed by its own site, use frame-ancestors 'self'. If there are official partners, include only the necessary domains, not overly broad wildcards.

form-action to limit form submission targets

CSP can also limit the destination of form submissions:

form-action 'self';

This helps prevent fake forms that are injected into the page and send credentials or user data to foreign domains. For websites using payment gateways, the official destination domains need to be explicitly added.

Do not enable blocking mode immediately

A common mistake when implementing CSP is making the rules too strict and then applying them directly to the production website. As a result, certain buttons may not work, fonts may disappear, payment forms may fail, or analytics may stop sending data.

Use Content-Security-Policy-Report-Only first. This mode does not block resources, but the browser logs violations so you can see which parts are not compliant.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'self';

Test important pages like login, checkout, admin dashboard, contact forms, and pages using visual editors. Open Developer Tools in the browser, check CSP violation messages, and note the sources that are indeed needed.

Distinguish between official resources and resources that just happen to appear. If there are scripts from unknown domains, do not rush to add them to the allowlist. Investigate which plugin or feature is calling them. It could be an old dependency, ad script, or a component that should be removed.

Implementation for WordPress and PHP

In WordPress, CSP should be applied after inventorying plugins and external services. Security plugins can help add headers, but website owners still need to understand the policy. Overly general headers can sometimes cause editor, preview, or payment integration features to fail.

If using PHP without a framework, headers can be sent before the HTML output:

<?php
header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'self';");
?>

In practice, configurations should ideally be managed at the server or middleware level to ensure consistency across all responses. Also, ensure that no output is sent before the header() function is called.

For Nginx or Apache configurations, the principle is the same: send headers from the server, then test all important flows. CSP via meta tags can be an option when server headers cannot be changed, but it does not support all CSP capabilities. For more comprehensive protection, response headers are still recommended.

What you can do now

  1. Inventory JavaScript, fonts, images, iframes, APIs, and third-party services used by the website.
  2. Create an initial policy that restricts resources to 'self' and disables objects with object-src 'none'.
  3. Implement the policy in Report-Only mode in staging or monitored production.
  4. Check violations on login pages, forms, checkout, and dashboards.
  5. Remove unnecessary dependencies before adding new domains to the allowlist.
  6. Once stable, switch to the Content-Security-Policy header and monitor errors after launch.

The Bottom Line

CSP is not a magic button that makes a website safe from all attacks. Input validation, output sanitization, session management, dependency updates, and access restrictions remain the main foundations.

However, CSP makes the browser not just a page viewer. The browser also helps enforce restrictions on which scripts and resources are allowed to run. Start gradually, use Report-Only mode, and consider every domain added to the allowlist as a security decision—not just a solution to make errors disappear.

Sources & Further Reading

Explore Also

– Rio Yotto @rioyotto