PHP2ShellCVE-2026-109618, CVE-2026-109619: critical RCE, exploited in the wild.Read the advisory
Harden the front end

Block injected scripts at the edge, even if PHP is compromised.

With code execution on your PHP origin, an attacker can add a wallet drainer or card skimmer to every page it serves. A Content-Security-Policy set at your CDN or reverse proxy still reaches the browser, and the browser refuses scripts from any host the policy doesn't list. Build one here, run it in Report-Only, then enforce it.

Runs in your browser. Nothing you type here is sent anywhere.

Policy builder

Pick a starting point, then trim it to what you use.

Start from a preset

Every script, style, font and image comes from your own origin. This is the strict default.

Mode

Report-Only: nothing is blocked. The browser reports what would have been. Start here.

Sources per directive

Separate sources with spaces or new lines. Keywords go in single quotes ('self', 'none'); hosts and schemes don't (https://cdn.example.com, wss:). Leave a box empty to drop that directive. Choosing a preset replaces what's in the boxes.

Other settings
response headerreport-onlyContent-Security-Policy-Report-Only
Content-Security-Policy-Report-Only
default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
_headers
/*
  Content-Security-Policy-Report-Only: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests

Cloudflare Pages and Netlify read _headers from the root of the published directory. Cloudflare Pages doesn't apply _headers to responses from Pages Functions; set the header in the function instead.

Warnings
  • infoReport-Only: nothing is blocked. The browser reports what would have been. Start here, then enforce.
  • Nothing risky stands out. Still run it in Report-Only before you enforce it.
About this preset: Static site
  • default-src 'none' means anything you forget to list is blocked, so every directive you need is spelled out.
  • img-src allows data: because inline SVG and favicons are often embedded that way; drop it if you never use data URLs.
  • base-uri 'none' and frame-ancestors 'none' close the <base> hijack and clickjacking, and neither falls back to default-src.
Where these hosts come from

How it works

What an edge policy stops, and what it doesn't.

Why a header set at the edge survives a compromised origin

When PHP is compromised, the attacker controls whatever the origin sends: the HTML, the scripts it serves, and its response headers. A header added by your CDN or reverse proxy is outside that reach, as long as the edge runs on a different system with different credentials. The browser gets your policy on every response either way.

The origin can't loosen it, either. A page can carry several policies at once (headers and <meta> tags), and every one of them is enforced: a resource has to be allowed by all of them, so adding a policy can only restrict further (CSP3 §8.1). On Cloudflare, a Set static response header rule goes further and overrides any header with the same name, so the origin's own value is replaced.

So when an injected <script src="https://drainer.example/x.js"> or an inline <script> shows up in your page, a policy with script-src 'self' and no 'unsafe-inline' stops it from running. A tight connect-src also limits where a script that does run can send what it collects.

Apache with mod_php is not an edge.
If the header comes from the same Apache that runs PHP, the attacker is on that server. At the very least set it in the virtual host, not in .htaccess, and set AllowOverride None and AllowOverrideList None, which makes Apache ignore .htaccess files entirely. Otherwise a dropped .htaccess can use Header, which is allowed there under FileInfo. A CDN or a proxy on another machine is better.

What it can't do

  • It allows by origin, not by content. Any script from a source you allow will run. If your JavaScript is served by the same PHP origin that was compromised, 'self' includes every file the attacker can write, from edited bundles to a new .js in an upload directory. Serve static assets from a separate origin the PHP host can't write to, and keep upload directories from serving scripts.
  • It doesn't protect the server. Code running as your PHP worker can still read keys, database credentials and sessions. Patch, then rotate (see Fixes and Crypto & Solana).
  • It doesn't stop changes to your own content. The attacker can still rewrite text, swap a deposit address in the HTML, or add a form that posts to your own origin.
  • It doesn't cover browser extensions. The spec says a policy shouldn't interfere with add-ons and extensions (CSP3 §9.1), so wallet extensions run whatever your policy says.
  • A static nonce is no nonce. A nonce must be freshly generated for every response and should be at least 128 bits (CSP3 §7.1). A header fixed at the edge can't do that, so use hashes for inline code you must keep.
  • https:// and wss:// are listed separately. CSP upgrades http to https and ws to wss, but an https:// source doesn't match a wss:// URL (CSP3 §6.7.2.9). 'self' does cover wss: on your own origin (CSP3 §1.3).
  • A <meta> tag is a weaker fallback. Your origin serves it, so a compromised origin can drop it. Browsers also ignore frame-ancestors, report-uri and sandbox in it, and it doesn't apply to anything above it in the document (CSP3 §3.3).

Pin third-party scripts with Subresource Integrity

If you load a script from a CDN, add an integrity hash so the browser refuses the file if its contents change. A mismatch is treated as a network error, and the accepted algorithms are sha256, sha384 and sha512. Generate the hash from the exact file you reviewed:

Generate a sha384 integrity hash
openssl dgst -sha384 -binary file.js | openssl base64 -A

Prefix the output with sha384- and add crossorigin="anonymous". Cross-origin SRI needs CORS, so the CDN has to send Access-Control-Allow-Origin; without crossorigin the request always fails.

HTML
<script src="https://cdn.example.com/lib@1.2.3/lib.min.js"
        integrity="sha384-PASTE_THE_HASH_HERE"
        crossorigin="anonymous"></script>

To check a live file against the hash in your page, hash what the CDN serves right now and compare the two strings:

Verify what the CDN serves
curl -s https://cdn.example.com/lib@1.2.3/lib.min.js | openssl dgst -sha384 -binary | openssl base64 -A

Pin a versioned URL, never a "latest" alias, or the hash breaks on the next release. SRI lives in your HTML, so it protects you from a compromised CDN, not from a compromised origin that rewrites the tag. That's what the edge CSP is for.

Test with Report-Only before you enforce

Content-Security-Policy-Report-Only reports violations without blocking anything. It has to be a header; it isn't supported in a <meta> tag.

  1. Ship the policy with the Report-Only header name (switch the mode above).
  2. Click through every kind of page: home, login, checkout or wallet connect, account pages and anything embedded. Watch the browser console, or the endpoint you set as the report URL.
  3. For each violation, decide whether it's yours. Add hosts you use; move inline code into files or allow it by hash. Don't reach for 'unsafe-inline' in script-src.
  4. Switch to the enforcing header name once the reports are quiet. You can keep sending a Report-Only header with a stricter draft alongside the enforced one, since both are applied.

For reports to be sent anywhere, report-to names an endpoint defined in the Reporting-Endpoints header. report-uri is deprecated but still worth sending next to it until report-to works everywhere; browsers that support report-to ignore report-uri. The builder adds both when you fill in a report URL.