Bezpečnost frontendu: XSS, CSP a další hrozby v praxi
Jak vzniká XSS a jak mu zabránit, nastavení Content-Security-Policy, bezpečné cookies, tokeny v prohlížeči, clickjacking a závislosti z npm.
Frontend běží na zařízení uživatele, takže útočník má k němu plný přístup – může číst kód, měnit požadavky i obcházet validaci. Úkolem frontendu tedy není „schovat tajemství”, ale nedovolit, aby se do stránky dostal cizí kód, a omezit škody, kdyby se to přece jen stalo.
Pravidlo nula: frontend nic netají
- API klíče, hesla a tajné tokeny do frontendového kódu nepatří. Proměnné s prefixem
VITE_,NEXT_PUBLIC_apod. jsou viditelné v bundlu. - Validace a oprávnění v prohlížeči jsou jen pohodlí pro uživatele. Server musí vše ověřit znovu.
- Skryté tlačítko „Smazat” není ochrana. Endpoint za ním musí kontrolovat, kdo ho volá.
XSS: Cross-Site Scripting
XSS vzniká, když se data od uživatele dostanou do stránky jako kód. Útočníkův skript pak běží v kontextu vaší domény – vidí stejná data jako uživatel a může jeho jménem posílat požadavky.
Typický zranitelný kód
// ❌ komentář od uživatele vložený jako HTML
commentEl.innerHTML = comment.text;
// ❌ parametr z URL
const q = new URLSearchParams(location.search).get('q');
results.innerHTML = `Výsledky pro: ${q}`;
Stačí ?q=<img src=x onerror=alert(document.cookie)> a skript se spustí. (Tag <script> vložený přes innerHTML se nespustí, ale atributy událostí ano.)
Oprava
// ✅ text místo HTML
commentEl.textContent = comment.text;
// ✅ skládání DOM
const p = document.createElement('p');
p.textContent = `Výsledky pro: ${q}`;
results.replaceChildren(p);
Nebezpečná místa („sinky”), kterým se vyhýbejte nebo je používejte jen s ověřenými daty: innerHTML, outerHTML, insertAdjacentHTML, document.write, eval, new Function, setTimeout s řetězcem a atributy href/src s hodnotou od uživatele.
Pozor na odkazy
link.href = userInput; // ❌ userInput = "javascript:alert(1)"
Ověřte protokol:
function safeUrl(input) {
try {
const url = new URL(input, location.origin);
return ['http:', 'https:', 'mailto:'].includes(url.protocol) ? url.href : '#';
} catch {
return '#';
}
}
Frameworky pomáhají, ale ne úplně
React, Vue i Angular automaticky escapují hodnoty v šablonách – {comment.text} je bezpečné. Únikové dveře však existují:
- React:
dangerouslySetInnerHTML, - Vue:
v-html, - Angular:
bypassSecurityTrustHtml.
Pokud opravdu potřebujete vykreslit HTML od uživatele (např. formátovaný text z editoru), prožeňte ho sanitizační knihovnou DOMPurify:
import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(userHtml);
Vlastní regex na odstranění <script> není sanitizace – existují desítky způsobů, jak ho obejít.
Content-Security-Policy: druhá linie obrany
CSP je HTTP hlavička, která prohlížeči říká, odkud smí stránka načítat skripty, styly a další zdroje. I když se do stránky dostane škodlivý kód, CSP může zabránit jeho spuštění.
Rozumný výchozí bod pro web bez inline skriptů:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; connect-src 'self' https://api.example.cz; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
| Direktiva | Význam |
|---|---|
default-src 'self' |
výchozí: jen vlastní doména |
script-src |
odkud smí být skripty; bez 'unsafe-inline' neběží inline skripty ani onclick atributy |
connect-src |
kam smí jít fetch, WebSocket |
object-src 'none' |
zákaz pluginů |
base-uri 'self' |
brání podvržení <base> tagu |
frame-ancestors 'none' |
stránku nelze vložit do iframe (ochrana proti clickjackingu) |
Inline skripty: nonce
Pokud potřebujete inline skript (např. proti bliknutí u dark mode), použijte nonce – náhodnou hodnotu generovanou serverem pro každý požadavek:
Content-Security-Policy: script-src 'self' 'nonce-R4nd0mH0dn0ta'
<script nonce="R4nd0mH0dn0ta">/* … */</script>
Útočník nonce nezná, takže jeho vložený skript neprojde. Statický, neměnný nonce nemá smysl. Alternativou pro statické inline skripty je hash ('sha256-…'), který vám prohlížeč vypíše v chybě konzole.
Nasazení bez rozbití webu
Začněte v režimu, který jen hlásí, ale neblokuje:
Content-Security-Policy-Report-Only: default-src 'self'; …
V konzoli DevTools uvidíte, co by se zablokovalo. Upravte politiku, a teprve až je konzole čistá, přepněte na ostrou hlavičku.
Nginx:
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
Pozor: add_header v bloku location ruší hlavičky zděděné ze server. Výsledek ověřte přes curl -I https://vas-web.cz.
Další užitečné hlavičky
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
HSTS nastavujte, až když HTTPS spolehlivě funguje na všech subdoménách – prohlížeč si ho pamatuje po dobu max-age.
Cookies a tokeny
Kde držet přihlašovací token? Cokoli v localStorage přečte jakýkoli skript na stránce – tedy i útočníkův při XSS. Bezpečnější je session cookie, kterou JavaScript vůbec nevidí:
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
HttpOnly– nečitelné z JavaScriptu,Secure– jen přes HTTPS,SameSite=Lax– cookie se neposílá s většinou požadavků z cizích webů, což omezuje CSRF. Pro citlivé akce přidejte na serveru CSRF token.
Odkazy na cizí weby a iframy
Odkazy s target="_blank" dnes prohlížeče automaticky chrání (výchozí noopener), explicitní rel="noopener noreferrer" ale neuškodí. U iframe s cizím obsahem použijte atribut sandbox a povolte jen nutné schopnosti.
Závislosti a skripty třetích stran
Každý npm balíček a každý externí skript běží se stejnými právy jako váš kód.
npm audit
npm audit fix
npm ci # v CI instaluje přesně podle package-lock.json
- Commitujte
package-lock.jsona v CI používejtenpm ci. - Před přidáním balíčku zkontrolujte jeho popularitu a údržbu; pozor na překlepy v názvech (typosquatting).
- Skripty z cizích CDN načítejte se Subresource Integrity:
<script src="https://cdn.example.com/[email protected]/lib.min.js"
integrity="sha384-…" crossorigin="anonymous"></script>
Pokud se obsah souboru na CDN změní, prohlížeč ho odmítne spustit.
Kontrolní seznam
- [ ] žádné tajné klíče ve frontendovém kódu
- [ ] uživatelská data jen přes
textContentnebo escapování frameworku - [ ]
v-html/dangerouslySetInnerHTMLjen s DOMPurify - [ ] CSP bez
'unsafe-inline'a'unsafe-eval'vescript-src - [ ] session v
HttpOnly; Secure; SameSitecookie - [ ] bezpečnostní hlavičky ověřené přes
curl -I - [ ]
npm auditv CI a SRI pro externí skripty
Pokud chcete projít existující kód, nahrajte projekt do ColdHost AI a v režimu Programování požádejte o kontrolu míst, kde se do DOM vkládá HTML nebo URL od uživatele – výsledky ale vždy ověřte sami.