A Content-Security-Policy (CSP) header tells the browser which sources it may load, and by default forbids inline <script>. It is a second wall behind output escaping: if an XSS payload slips through, a good CSP stops the browser running it. Security Headers and CORS sets a static policy in Apache 129 ; from PHP you can emit a fresh nonce per response, letting your own inline scripts run while blocking any the attacker injected.
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: default-src 'self'; "
. "script-src 'self' 'nonce-$nonce'; object-src 'none'; base-uri 'none'; "
. "frame-ancestors 'none'");
?><!doctype html><meta charset="utf-8">
<script nonce="<?= $nonce ?>">console.log('this inline script is allowed');</script>
<script>console.log('this one is blocked by the browser');</script>Fetching the page with curl 3,008 -i shows the response header:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-ESBANq93GB+0ECBriyn5Kg=='; object-src 'none'; base-uri 'none'; frame-ancestors 'none' Content-Type: text/html; charset=UTF-8
The nonce is random per request, so an injected <script> cannot carry it and the browser refuses to run the second, un-nonced script. object-src 'none' kills plugin vectors, base-uri 'none' stops a <base> tag rewriting relative URLs, and frame-ancestors 'none' blocks clickjacking (the modern X-Frame-Options). Roll a policy out first as Content-Security-Policy-Report-Only, watch what breaks, then enforce it; avoid 'unsafe-inline' and 'unsafe-eval'. A nonce is the PHP way; a static file server is limited to hashes or the allow-listing of Security Headers and CORS.