Lesson 11 / الدرس 11

Six headers and a certificate / ست ترويسات وشهادة

A short block of headers, written once, closes several whole classes of attack. It is the best return on effort available anywhere in this course — and almost every small site ships without it because nothing visibly breaks when it is missing.

كتلة قصيرة من الترويسات، تُكتب مرة، تغلق أصنافًا كاملة من الهجمات. وهي أفضل عائد على جهد متاح في هذه الدورة كلها — ومع ذلك يُشحن كل موقع صغير تقريبًا بدونها لأن شيئًا لا ينكسر ظاهريًا حين تغيب.

HTTPS is free and automatic now, so the remaining question is not whether to use it but whether anything is still reachable without it. A site that answers on both schemes has a plain-text version of itself available to anyone on the network, and a session cookie sent once over that version is a session someone else can use.

The block, and what each line prevents

<?php
// This site's src/bootstrap.php, shortened. Sent on every response.

header(
    "Content-Security-Policy: default-src 'self'; "
    . "script-src 'self' 'nonce-" . csp_nonce() . "'; "
    . "img-src 'self' data:; object-src 'none'; "
    . "form-action 'self'; frame-ancestors 'none'"
);

header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');

// Only over HTTPS. Sent on a plain-HTTP response it is ignored, and on
// a local http host it would pin a scheme the dev site does not serve.
if (is_https()) {
    header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
That is the whole of it. Fewer than a dozen lines, sent once per request, and nothing about the rest of the application has to change — which is why it is the first thing to do rather than the last.
HeaderWhat it stops
Content-Security-PolicyInjected script running — a second wall behind escaping, for the hole you missed
Strict-Transport-SecurityThe browser ever using plain HTTP again, including on the first click of a typed address
frame-ancestors / X-Frame-OptionsYour site being loaded invisibly inside someone else's and clicked through
X-Content-Type-Options: nosniffAn uploaded file being guessed into a type it was not served as
Referrer-PolicyFull URLs — which can hold ids and tokens — leaking to every site you link to
form-actionAn injected form posting your visitor's data to somebody else's server

The CSP row is worth reading twice. It does not replace escaping — it is what catches the one place you forgot to escape, which on a site of any size there eventually is. Defences that assume you got everything right are not defences.

A policy that fits the site you have

A strict CSP is a real constraint, and it is worth knowing that before you write the application rather than after. Inline <script> blocks, onclick attributes, eval, and scripts loaded from a CDN all stop working under script-src 'self'. On this site that is why every animation is CSS rather than JavaScript, and why nothing loads from a CDN — the policy came first and the design followed it.

Check yourself / اختبر نفسك

1. What does a Content-Security-Policy protect against that escaping does not?

2. Why start with a short max-age on Strict-Transport-Security?

3. Why does a strict CSP need to be decided before the application is built?