Programování

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.

12. 10. 2026 · 5 min čtení

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.json a v CI používejte npm 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 textContent nebo escapování frameworku
  • [ ] v-html / dangerouslySetInnerHTML jen s DOMPurify
  • [ ] CSP bez 'unsafe-inline' a 'unsafe-eval' ve script-src
  • [ ] session v HttpOnly; Secure; SameSite cookie
  • [ ] bezpečnostní hlavičky ověřené přes curl -I
  • [ ] npm audit v 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.