zranitelnost.cz

Návod · oblast 04 · Bezpečnostní hlavičky

Bezpečnostní hlavičky: CSP, HSTS a spol. krok za krokem

Bezpečnostní hlavičky neopraví chybu ve vašem webu. Dělají něco jiného a v lecčems cennějšího: určují, co všechno smí prohlížeč vašeho zákazníka udělat ve chvíli, kdy se na stránce objeví cizí skript nebo kdy váš web někdo vloží do svého rámu. Je to jeden blok konfigurace, který napíšete jednou a chrání všechny stránky najednou, i ty, které na web přibydou za dva roky.

aktualizováno 19. 9. 2026 · čtení na 9 minut · hotové hodnoty pro nginx, Apache i Next.js

Proč to řešit

Protože je to nejlevnější vrstva obrany, jakou máte. Zranitelnost v pluginu opravíte, a za půl roku přijde další. Hlavičky platí plošně a pořád: dobře napsaná CSP znamená, že i když se někam vloudí cizí skript, prohlížeč ho odmítne spustit, protože nepřišel z domény, kterou jste povolili.

Protože škodu odnese návštěvník. U typického zneužití nejde o váš server. Jde o přihlášeného zákazníka, kterému se z prohlížeče vytáhne session, o podvržený platební formulář uvnitř vaší stránky nebo o tlačítko, na které klikne v cizím rámu s tím, že je vaše.

A protože je to vidět zvenku. Hlavičky si přečte každý, kdo se podívá, žádné přístupy k tomu nepotřebuje. Včetně korporátního zákazníka, který si vás před podpisem prověřuje, a včetně útočníka, který si vybírá, kde to půjde snadněji.

tohle se plete nejčastěji

Hlavička není náhrada za opravu. CSP nezabrání tomu, aby váš plugin měl chybu, zabrání jen tomu, aby se ta chyba dala využít k načtení skriptu z cizí domény. Opravovat se musí obojí, hlavičky jsou ale hotové dneska odpoledne, zatímco na aktualizaci systému čekáte na dodavatele.

Šest hlaviček a co každá dělá

Přesně tahle šestice se objevuje v oblasti „Bezpečnostní hlavičky“ v našem skenu. Každá řeší jiný typ útoku a žádná nenahradí ostatní:

Strict-Transport-SecurityHSTS
Řekne prohlížeči, ať se s vaší doménou odteď baví výhradně po HTTPS. Bez ní stačí útočníkovi na sdílené wi-fi to, že první požadavek letí nešifrovaně.
Content-Security-PolicyCSP
Výčet domén, odkud se smí načíst skripty, styly, obrázky a rámy. Nejúčinnější a zároveň nejpracnější z celé šestice: je to ta hlavička, která z nalezené chyby udělá neškodnou.
X-Frame-Optionsclickjacking
Zakáže vložit váš web do cizího rámu. Bez ní si podvodník přetáhne vaši pokladnu do svojí stránky a překryje ji vlastními tlačítky. V moderních prohlížečích ji nahrazuje direktiva frame-ancestors v CSP.
X-Content-Type-Optionsnosniff
Zakáže prohlížeči hádat typ souboru podle obsahu. Nahraný „obrázek“, který je ve skutečnosti skript, se tak nespustí.
Referrer-Policysoukromí
Určuje, kolik z adresy vaší stránky se dozví cizí web, na který návštěvník klikne. Bez ní odchází ven celá URL včetně parametrů, tedy občas i tokenu z e-mailu.
Permissions-Policyhardware
Vypne funkce, které váš web stejně nepoužívá: kameru, mikrofon, polohu. Týká se to i skriptů třetích stran, které na stránce běží.

K tomu patří ještě sedmá věc, která se hlavičkou neposílá, ale chodí stejnou cestou: příznaky u cookies. Tu si necháme na poslední krok.

krok 1

Zjistěte, co web posílá teď

Nejrychleji přes bezplatný sken na této stránce: oblast „Bezpečnostní hlavičky“ vypíše, co máte nastavené, co chybí a jestli web vůbec přesměrovává na HTTPS. V terminálu zjistíte totéž jedním příkazem:

terminál

curl -sI https://vase-firma.cz

Na Windows v PowerShellu: (Invoke-WebRequest -Method Head https://vase-firma.cz).Headers

Zkuste to hned na několika adresách: kromě hlavní domény i variantu s www, API subdoménu a stránku za přihlášením. Hlavičky obsluhuje ten virtuální host, který požadavek vyřizuje, takže se mezi nimi běžně liší. Nejhůř na tom bývá to, na co si nikdo nevzpomene.

krok 2

Nejdřív zavřete nešifrované http://

Dokud web odpovídá i na http://, nemá zbytek smysl řešit. Útočník na sdílené wi-fi nepotřebuje lámat šifrování, stačí mu ten jeden nezabezpečený požadavek na začátku. Přesměrování patří do samostatného bloku, který neumí nic jiného:

nginx

server {
    listen 80;
    server_name vase-firma.cz www.vase-firma.cz;
    return 301 https://$host$request_uri;
}

Apache · .htaccess

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]

Ověření: curl -sI http://vase-firma.cz | head -1 musí vrátit 301 a Location s https://.

krok 3

Čtyři řádky, které nasadíte bez obav

Tyhle čtyři hlavičky nasazujeme jako první, protože u drtivé většiny webů nic nerozbijí a přitom zavřou celé třídy útoků. Konfiguraci dejte na jedno místo pro celý web, ne k jednotlivým stránkám:

nginx · do server bloku

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

Pozor na past nginxu: jakmile použijete add_header uvnitř location bloku, zruší se tím všechny hlavičky zděděné z nadřazeného bloku. Buď je mějte jen na jednom místě, nebo je v každém vnořeném bloku zopakujte.

Apache · .htaccess nebo konfigurace virtuálního hostu

<IfModule mod_headers.c>
  Header always set X-Content-Type-Options "nosniff"
  Header always set X-Frame-Options "SAMEORIGIN"
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>

Na sdíleném hostingu bývá .htaccess jediná cesta. Když hlavičky nepřibydou, nemá web zapnutý mod_headers a je potřeba napsat podpoře.

Next.js · next.config.ts

async headers() {
  return [
    {
      source: "/:path*",
      headers: [
        { key: "X-Content-Type-Options", value: "nosniff" },
        { key: "X-Frame-Options", value: "SAMEORIGIN" },
        { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
        {
          key: "Permissions-Policy",
          value: "camera=(), microphone=(), geolocation=()",
        },
      ],
    },
  ];
}

Hodnotu SAMEORIGIN zvolte, pokud si vlastní stránky někde vkládáte do rámu sami. Když ne, je správně DENY. A pokud váš web legitimně vkládá do rámu partner (rezervační widget, kalkulačka u dodavatele), nesmí být ani jedno z toho: v takovém případě potřebujete frame-ancestors s jeho doménou, o kterém je řeč v kroku 5.

krok 4

HSTS zavádějte postupně, ne rovnou na rok

HSTS je jednosměrka, a proto se nasazuje po fázích. Jakmile si prohlížeč vaši doménu poznamená, bude s ní po celou dobu platnosti mluvit jen šifrovaně, a to i tehdy, když certifikát propadne. Návštěvník takovou chybu nemá jak „odklepnout“, prostě se na web nedostane — proto HSTS nasazujte až ve chvíli, kdy se vám certifikáty obnovují samy.

Fáze A, zkouška (pár hodin). Krátká platnost, ze které se dá vycouvat během oběda:

hodnota hlavičky

max-age=300

Fáze B, ostrý provoz (cílový stav). Rok a platnost i pro subdomény. Než tohle nasadíte, projděte seznam subdomén ze skenu a ověřte, že každá z nich umí HTTPS. Právě tady se láme staré intranet.vase-firma.cz, na které nikdo nemyslel:

hodnota hlavičky

max-age=31536000; includeSubDomains

Sken hlásí „HSTS má krátkou platnost“, dokud je max-age pod 180 dny. Přechodná fáze A tedy varování vyvolá, a je to tak správně.

Fáze C, preload (volitelná, a pořádně si ji rozmyslete). Přidáním preload a zápisem do seznamu se doména dostane napevno do samotných prohlížečů, takže jsou chráněné i první návštěvy. Zápis je otázka minut, vyřazení trvá měsíce a projeví se až s další verzí prohlížeče. Pro běžný firemní web to není nutnost, pro banku nebo e-shop s přihlášením ano.

krok 5

CSP: nejdřív jen měřte, teprve potom blokujte

Content-Security-Policy je z celé šestice zdaleka nejúčinnější a jako jediná umí web rozbít. Proto existuje režim, ve kterém prohlížeč nic nezakáže a jen hlásí, co by zakázal. Začněte vždycky tam: stejná hodnota, jiný název hlavičky.

Content-Security-Policy-Report-Only

default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
report-uri /api/csp-report;

Do jednoho řádku hlavičky se to poskládá oddělováním středníky. Hlášení posílá Firefox a Safari na report-uri, Chrome na endpoint z hlavičky Reporting-Endpoints (direktiva report-to) – uveďte klidně obojí.

Pak se týden dívejte, co chodí. Skoro jistě zjistíte, že na webu běží víc cizích domén, než jste čekali: analytika, písma, chat, mapy, platební brána, pixel z kampaně, kterou někdo pouštěl předloni. Každou, kterou opravdu chcete, dopište do příslušné direktivy. Co v hlášeních zbyde a nepoznáváte to, je samo o sobě zajímavý nález.

Až bude týden bez překvapení, přejmenujte hlavičku na Content-Security-Policy a politika začne platit.

Dvě věci, na kterých to obvykle uvázne. První je 'unsafe-inline' ve script-src. Přidat ho je svůdné, protože tím CSP okamžitě přestane vadit, jenže povolit vložený skript je přesně to, proti čemu má CSP chránit. U vlastní aplikace se to řeší jednorázovým kódem pro každý požadavek (nonce) nebo otiskem konkrétního skriptu (hash), u style-src je 'unsafe-inline' přijatelný kompromis, protože vložený styl škodí řádově míň.

Druhá je hotový systém s hromadou pluginů, kde inline skripty vyrábí kdekdo. Tam bývá realistické začít u obrázků, rámů a formulářů, tedy u direktiv img-src, frame-ancestors a form-action, nechat script-src zatím v Report-Only a utahovat ho postupně. I neúplná CSP je víc než žádná.

krok 6

Cookies a ověření, že to funguje

Cookies se nenastavují ve webserveru, ale v aplikaci, která je vydává. Přihlašovací cookie má mít tři příznaky:

Set-Cookie

Set-Cookie: session=…; Secure; HttpOnly; SameSite=Lax; Path=/

Secure znamená, že se cookie nikdy nepošle nešifrovaně. HttpOnly zařídí, že ji nepřečte JavaScript, takže ji neukradne ani skript, který se na stránku vloudí. Cookie, kterou váš web čte v prohlížeči (souhlas s cookies, A/B test), HttpOnly mít nemůže a nevadí to, pokud v ní není přihlášení. SameSite=Lax je dobrá výchozí hodnota, None použijte jen tam, kde cookie skutečně musí fungovat uvnitř cizí stránky, a vždy současně se Secure.

A na závěr ověření, ze tří stran. Pusťte znovu příkaz z prvního kroku nebo si nechte doménu proskenovat. Otevřete si konzoli prohlížeče a projděte klíčové stránky, jestli nepřibyly hlášky o zablokovaném obsahu. A hlavně si proklikejte to, co vydělává: přidání do košíku, pokladnu, přihlášení, vyhledávání, formulář. CSP se skoro nikdy neprojeví na úvodní stránce, ale až na třetím kroku objednávky, kde dodavatel kdysi použil vložený skript.

bonus na pět minut

Když už jste v konfiguraci, přidejte soubor /.well-known/security.txt (standard RFC 9116). Stačí pár řádků s kontaktem, na který se má obrátit ten, kdo u vás najde chybu, a s datem platnosti, které je povinné: Contact: mailto:security@vase-firma.cz a Expires: 2027-09-19T00:00:00.000Z. Jinak takový nález skončí ve firemní schránce info@, kde ho nikdo nepřečte.

Pět chyb, které v kontrolách vidíme nejčastěji

CSP s unsafe-inline ve script-src

Nejčastější CSP, jakou vidíme. Sken i online testy ji odškrtnou jako splněnou, jenže povolit vložený skript je přesně to, proti čemu má CSP chránit. Užší politika bez inline skriptů je k něčemu, široká jen vypadá.

HSTS s preload dřív, než všechny subdomény umí HTTPS

Zápis do seznamu prohlížečů se dělá snadno, odstranění trvá měsíce a projeví se až s jejich novou verzí. Do té doby je každá subdoména bez platného certifikátu pro návštěvníky nedostupná a nejde to obejít.

Hlavičky jen na hlavní doméně

Varianta s www, API subdoména, staging i starý web na subdoméně běží obvykle na jiném virtuálním hostu, kde se konfigurace nikdy nedoplnila. Útočníkovi stačí ten nejslabší z nich.

X-Frame-Options: ALLOW-FROM

Hodnota, která nikdy pořádně nefungovala a dnes ji prohlížeče ignorují úplně. Web tedy nemá proti clickjackingu žádnou ochranu, i když to na první pohled vypadá jinak. Nahrazuje ji frame-ancestors v CSP.

Tatáž hlavička nastavená na dvou místech

Jednou v pluginu nebo aplikaci, podruhé ve webserveru nebo na CDN. U CSP pak platí průnik obou politik a rozbije se něco, co ani v jedné z nich zakázané není. Mějte jedno místo, kde se hlavičky nastavují.

Nechte si s tím poradit

Napsat CSP pro web, na kterém běží šest cizích služeb, je zdlouhavá práce. Doplňte si do následujícího promptu, co váš web používá, a nechte si první verzi připravit, ať už v ChatGPT, Claude, nebo kdekoli jinde. Stejné prompty přikládáme u nálezů v placeném reportu.

AI prompt · zkopírujte do ChatGPT nebo Claude

Spravuji web vase-firma.cz. Běží na [nginx / Apache / sdílený hosting bez přístupu ke konfiguraci] a je postavený na [WordPress / vlastní aplikace / …]. Na stránkách mám tyhle externí služby: [analytika, fonty, chat, platební brána, …]. Tady jsou hlavičky, které web teď posílá: [vlož výstup curl -sI https://vase-firma.cz]. Navrhni mi kompletní konfiguraci bezpečnostních hlaviček přesně v syntaxi pro [nginx / Apache]: HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy a Permissions-Policy. Content-Security-Policy napiš nejdřív ve variantě Report-Only a bez unsafe-inline ve script-src a u každé direktivy vysvětli, kterou z mých služeb povoluje. Nakonec mi napiš, co si mám po nasazení proklikat a podle čeho poznám, že se něco rozbilo.

Výsledek nasazujte vždy nejdřív v režimu Report-Only podle kroku 5. Jazykový model nemá jak vědět, co všechno se vám na stránkách načítá, a nejčastěji zapomene právě na skript, který tam někdo přidal před rokem.

Jak je na tom váš web?

Hlavičky jsou jen jedna ze šesti oblastí, které kontrolujeme. Zadejte doménu a během chvíle uvidíte i certifikáty, subdomény a nastavení e-mailu. Zdarma a bez registrace.