Security

AuthN/AuthZ, JWT, OWASP, IDOR, hashování hesel.

Bezpečnost není funkce, kterou přidáš nakonec — je to způsob, jakým přemýšlíš o každém endpointu. Dvě věty, které když si zapamatuješ, vyhneš se většině začátečnických děr: nikdy nevěř tomu, co přijde od klienta, a nedávej nikomu víc oprávnění, než nezbytně potřebuje. Zbytek kapitoly tyhle dvě myšlenky rozvíjí.


Pár pojmů na úvod

  • Autentizace (authentication) = ověření, kdo jsi (přihlášení heslem, tokenem…).
  • Autorizace (authorization) = ověření, co smíš (máš právo na tuhle akci/data?).
  • Token = digitální „lístek", kterým se po přihlášení prokazuješ u každého dalšího requestu.
  • Hash = jednosměrná přeměna textu na otisk — z hesla spočítáš otisk, ale z otisku heslo zpět nezískáš.
  • Útočník = kdokoli, kdo zkouší tvůj systém zneužít (a předpokládej, že to zkouší).

Autentizace vs autorizace (nepleť si)

  • Autentizace (AuthN) = „kdo jsi?" Když selže → HTTP 401.
  • Autorizace (AuthZ) = „co smíš?" Když selže → HTTP 403.

Pořadí: nejdřív zjisti, kdo uživatel je (AuthN), pak co smí (AuthZ). A obojí na serveru, u každé operace, co mění data. (Rozdíl 401 vs 403 znáš z Foundations.)

RBAC vs ABAC — jak řídit, kdo co smí

Jakmile řešíš autorizaci pro víc lidí, potřebuješ systém, jak práva přidělovat:

  • RBAC (Role-Based Access Control) — práva se přidělují přes role. Uživatel má roli (admin, editor, viewer) a role určuje, co smí. Jednoduché a stačí na většinu aplikací.
  • ABAC (Attribute-Based Access Control) — práva se odvozují z atributů (vlastností uživatele, zdroje, kontextu): „editor smí upravit dokument, jen pokud patří do jeho oddělení a je pracovní doba". Mocnější, ale složitější.

Začni s RBAC (role). Po ABAC sáhni, až ti pravidla přerostou jednoduché role. A ať použiješ cokoli, kontrola se musí dít na serveru a pro každý zdroj (jinak hrozí IDOR, viz níže).


Session vs token (JWT)

Po přihlášení si musí server nějak pamatovat, že jsi to ty. Dva přístupy:

Session (na serveru)Token / JWT (u klienta)
Kde je stavServer si drží záznam o přihlášeníStav nese sám token (server si nic nepamatuje)
Odhlášení / zneplatněníSmažeš session → hned neplatíTěžké — token platí, dokud nevyprší

JWT (čti „džot") je takový token. Je podepsaný (server pozná, že ho nikdo nezměnil), ale není šifrovaný → nedávej do něj nic tajného, protože se dá přečíst. Aby se omezila škoda při krádeži, používá se krátká platnost (token brzy vyprší) plus „obnovovací" token, který jde zneplatnit.

⚠️ „Podepsaný" chrání jen když server podpis fakt ověří správným algoritmem a klíčem. Klasické JWT díry: token s alg: none (žádný podpis — server ho nesmí přijmout) a záměna RS256/HS256 (útočník podepíše token veřejným klíčem). Proto nikdy nepiš ověřování JWT ručně, použij ověřenou knihovnu a zafixuj povolený algoritmus.

Kam s tokenem v prohlížeči? Do localStorage token nedávej — je čitelný přes JavaScript, takže ho útočník při XSS prostě přečte a ukradne. httpOnly cookie ho JavaScriptu nezpřístupní (krádež přes XSS odpadá), ale tím se vrací CSRF (cookie prohlížeč přiloží sám). Bezpečnější volba je proto **httpOnly cookie

  • CSRF ochrana** (token / SameSite).

OAuth / „přihlášení přes Google"

Když appka nabízí „přihlásit se přes Google/GitHub", používá k tomu OAuth — uživatel se přihlásí u Googlu a ten tvojí appce potvrdí, kdo to je, aniž bys ty kdy viděl jeho heslo. Pro tebe jako juniora hlavní pravidlo: OAuth nikdy neimplementuj od nuly, vždy použij ověřenou knihovnu — je to plné záludností, ve kterých se snadno udělá bezpečnostní díra.


Service-to-service auth — kdo ověří službu, ne uživatele

Doteď jsme řešili, jak ověřit uživatele. Ale jak ví služba A, že požadavek opravdu přišel od služby B, a ne od útočníka uvnitř sítě? „Je to z naší sítě, takže je to OK" je nebezpečný předpoklad (zero trust říká: nevěř ani vnitřní síti). Řeší se to mTLS (mutual TLS) — na rozdíl od běžného HTTPS, kde se certifikátem prokazuje jen server, se u mTLS prokazují obě strany, takže si služby navzájem ověří identitu. Ve větších systémech to obstarává service mesh / workload identita (SPIFFE). Pro juniora stačí znát pojem a princip „služby se ověřují taky, ne jen uživatelé".


CORS — co to je a co to NENÍ

CORS NENÍ ochrana tvého serveru. Je to pravidlo prohlížeče, které chrání uživatele před cizími weby — brání tomu, aby JavaScript na webu zly.cz z prohlížeče přihlášeného uživatele volal tvé API a četl odpovědi. Server žádné CORS pravidlo nedonutí: kdokoli s curlem nebo vlastním backendem tvé API zavolá bez ohledu na CORS (proto „v curlu to projde, v appce ne" — viz Foundations). Z toho plyne, že autentizaci a autorizaci CORS nenahradí — ty musí být na serveru tak jako tak. Častá díra je nastavit Access-Control-Allow-Origin: * spolu s credentials (cookies/Authorization) — to je tak nebezpečné, že to prohlížeč s * rovnou zakazuje; povoluj proto konkrétní originy (whitelist), ne hvězdičku.


OWASP Top 10 — nejčastější díry

OWASP je organizace, která udržuje seznam nejčastějších bezpečnostních chyb. Pár, které musíš znát:

  1. Broken Access Control (porušená kontrola přístupu) — uživatel se dostane k cizím datům. Nejčastější forma je IDOR: endpoint /orders/123 vrátí objednávku jen podle ID, tak si zkusím změnit URL na /orders/124 a vidím cizí objednávku. Obrana: na serveru vždy ověř, že data patří přihlášenému uživateli, ne jen že je přihlášený.
  2. Injection (vstříknutí kódu) — vstup od uživatele se omylem provede jako kód. Klasika je SQL injection, kde vstup změní tvůj databázový dotaz. Obrana: parametrizované dotazy / ORM (vstup se předá jako data, ne jako součást příkazu) — nikdy neslepuj dotaz z textu od uživatele:
    // ❌ ŠPATNĚ — slepený řetězec: vstup `' OR '1'='1` ti vrátí všechny uživatele
    db.query(`SELECT * FROM users WHERE email = '${email}'`)
    // ✅ SPRÁVNĚ — parametr: databáze bere `email` vždy jako data, nikdy jako SQL
    db.query('SELECT * FROM users WHERE email = $1', [email])
    
  3. XSS (Cross-Site Scripting) — útočník vloží do stránky <script>, který se spustí v prohlížeči jiného uživatele. Obrana: výstup vždy „escapuj" (zneškodni HTML znaky) a nikdy nevkládej do stránky neošetřený text od uživatele jako HTML.
  4. CSRF — cizí web pošle požadavek jménem tvého přihlášeného uživatele. Týká se hlavně autentizace přes cookie (prohlížeč ji přiloží sám); u API s tokenem v hlavičce Authorization CSRF obvykle nevzniká. Obrana: CSRF token. SameSite=Lax dnes nastavují prohlížeče defaultně, ale sám nestačí (nechrání všechny případy), takže u operací měnících stav stejně potřebuješ token.

Hesla a tajné údaje

  • Hesla NIKDY neukládej v čitelné podobě ani jako „obyčejný" rychlý hash (SHA-256). Použij bcrypt / argon2 / scrypt — jsou záměrně pomalé (zkoušet hesla po milionech se nevyplatí) a používají salt (náhodnou přísadu pro každého uživatele zvlášť, takže stejné heslo má jiný otisk). Detaily, co odlišují seniora: OWASP doporučuje variantu argon2id; bcrypt tiše ořízne heslo nad 72 bajtů (reálná díra); a navíc pepper (tajný klíč mimo databázi, přidaný ke každému heslu) tě chrání i v případě, že někdo ukradne celou databázi i se salty.
  • Nedávej najevo, které účty existují (user enumeration). Při neúspěšném loginu vracej stejnou chybu pro neexistujícího uživatele i pro špatné heslo („nesprávný e-mail nebo heslo") — jinak útočník z rozdílu odpovědí zjistí, které účty jsou platné, a může je cíleně napadat. Totéž platí pro registraci a reset hesla.
  • Secrets (API klíče, hesla k databázi) patří do konfigurace mimo kód (proměnné prostředí, secret manager) a nikdy do gitu — z historie se už nedají pořádně smazat.
  • Šifruj data jak při přenosu (HTTPS/TLS), tak v úložišti (disk/databáze).

Validace, rate limiting a povrch útoku

  • Validuj vstup na serveru, i když ho ověřuje i frontend — klient se vždy dá obejít. Lepší je whitelist („povol jen tohle") než blacklist („zakaž tohle").
  • Rate limiting (Patterny) brání hádání hesel a zahlcení → 429.
  • Princip nejmenších oprávnění (least privilege) — každý proces, uživatel i token má jen to, co opravdu potřebuje. Když ho někdo zneužije, napáchá míň škody.
  • Defense in depth (obrana do hloubky) — víc vrstev ochrany: když jedna selže, chytne to další.
  • Nezveřejňuj v chybách technické detaily (stack trace, SQL, verze knihoven) — útočníkovi to pomáhá.

Secrets, audit log a supply chain

Pár dalších věcí, které k bezpečnému backendu patří:

  • Secrets management — tajné údaje (klíče, hesla k databázi) nepatří do kódu ani do gitu. Drž je v proměnných prostředí, nebo líp v secret manageru (specializovaná služba), kde se dají rotovat (pravidelně měnit) a sledovat, kdo k nim sáhl. Když klíč unikne, vyměň ho. ⚠️ Rotuj bez výpadku: nový klíč naivně shodí všechny instance, co drží starý. Proto ať starý i nový klíč chvíli platí současně (overlapping validity) — nasaď nový, přepni produkci, teprve pak starý zruš. Je to přesně stejný princip jako expand-contract u databázových migrací.
  • Audit log — záznam o citlivých akcích („kdo, kdy, co změnil/smazal"). Když se něco stane, chceš vědět, kdo za tím byl. Ukládej ho odděleně a tak, aby nešel snadno přepsat.
  • CSP (Content Security Policy) — bezpečnostní hlavička, kterou řekneš prohlížeči, odkud smí načítat skripty. Je to další obrana proti XSS: i když útočník skript propašuje, prohlížeč ho nespustí, pokud není z povoleného zdroje.
  • Supply chain (dodavatelský řetězec) — tvoje appka závisí na stovkách cizích knihoven a kterákoli může mít díru, nebo být přímo napadená (kompromitovaný maintainer, typosquatting — reálný případ je backdoor v xz/liblzma z roku 2024). Proto: skenuj závislosti na známé zranitelnosti, pinuj verze (lockfile, hashe), aktualizuj a u větších projektů veď SBOM (Software Bill of Materials — seznam, co všechno reálně používáš; dnes i regulatorní požadavek). Cizí kód je tvoje odpovědnost.

A jeden způsob myšlení nad tím vším: threat modeling. Než něco postavíš, projdi „kdo je útočník, kde jsou hranice důvěry (trust boundaries) a co se může pokazit" (metoda STRIDE). Líp najít díru na papíře než v produkci.


Failure modes — nejčastější reálné díry

  • IDOR → endpoint ověří jen přihlášení, ne vlastnictví dat. Hodně častá chyba.
  • SQL injection → dotaz slepený z textu místo parametrů.
  • Token bez vypršení / nejde zneplatnit → ukradený token platí navždy.
  • Secrets v gitu → uniknou natrvalo (i z historie). Když se to stane, klíče vyměň.
  • Důvěra klientovi → validace jen na frontendu = žádná validace.

🛠️ Cvičení

  1. Najdi IDOR. Endpoint GET /orders/123 vrací objednávku jen podle ID. Popiš, jak ho zneužiju, a oprav to ověřením vlastnictví na serveru.
  2. Oprav SQL injection. Máš dotaz slepený z textu od uživatele. Přepiš ho na parametrizovaný a vysvětli, proč to injection zabrání.
  3. 401, nebo 403? U pěti situací urči správný kód: špatné heslo, vypršelý token, běžný uživatel chce admin stránku, neznámý uživatel, platný token bez oprávnění k akci.
  4. Hashování hesel. Vysvětli, proč SHA-256(heslo) nestačí, a co dělají bcrypt/argon2 jinak (salt, pomalost).
  5. Mini threat model. Pro nahrání profilové fotky vyjmenuj tři rizika (typ souboru, velikost, stažení z cizí URL) a obranu ke každému.
Náčrt řešení — rozbal, až si cvičení zkusíš sám
  1. Najdi IDORGET /orders/123 vrací objednávku jen podle ID, takže si změním URL na /orders/124 a vidím cizí objednávku; oprava je na serveru u každého requestu ověřit, že objednávka patří přihlášenému uživateli (WHERE id = ? AND user_id = ?), ne jen že je přihlášený. Pozor: nestačí ověřit autentizaci (že je přihlášený) — chybí autorizace na konkrétní zdroj, a tohle je jedna z nejčastějších reálných děr.
  2. Oprav SQL injection — dotaz slepený z textu ("... WHERE name = '" + vstup + "'") přepiš na parametrizovaný (... WHERE name = ? a vstup předej zvlášť jako parametr) nebo použij ORM; databáze pak vstup vezme jako data, nikdy jako součást příkazu. Pozor: nikdy neslepuj dotaz z textu od uživatele — a dej DB účtu jen nezbytná práva (least privilege), ať případný průnik napáchá míň škody.
  3. 401, nebo 403? — špatné heslo → 401 (neověřená identita), vypršelý token → 401, neznámý uživatel → 401; běžný uživatel na admin stránku → 403 (ví se kdo je, ale nesmí), platný token bez oprávnění k akci → 403. Pozor: 401 = „nevím, kdo jsi / neověřil ses", 403 = „vím kdo jsi, ale nesmíš" — pleteni těchhle dvou je klasická chyba.
  4. Hashování heselSHA-256(heslo) je rychlá a bez soli, takže útočník zkusí miliardy hesel za sekundu a předpočítané tabulky prolomí běžná hesla okamžitě; bcrypt/argon2 jsou záměrně pomalé (zkoušení se nevyplatí) a používají salt (náhodná přísada pro každého zvlášť → stejné heslo má jiný otisk). Pozor: doporučená varianta je argon2id a bcrypt tiše ořízne heslo nad 72 bajtů — drobnost, na které senior pozná juniora.
  5. Mini threat model — typ souboru: útočník nahraje skript místo obrázku → ověřuj reálný obsah/MIME (ne jen příponu) a povol whitelist formátů; velikost: obří soubor zahltí úložiště → limit velikosti a 429/odmítnutí; stažení z cizí URL: zadání interní adresy (SSRF) → URL nestahuj naivně, validuj a blokuj interní rozsahy. Pozor: spoléhej na whitelist („povol jen tohle") místo blacklistu a validuj na serveru — klient se vždy dá obejít.

🧠 Otázky & odpovědi

Jaký je rozdíl mezi autentizací a autorizací?

Autentizace ověřuje „kdo jsi" (heslo, token) → při selhání 401. Autorizace řeší „co smíš" (role, vlastnictví dat) → při selhání 403. Pořadí: nejdřív zjisti, kdo uživatel je, pak co smí. Obojí se musí dít na serveru a u každé operace, co mění data — frontend se dá obejít.

Co je IDOR a jak ho ošetříš?

IDOR (Insecure Direct Object Reference): endpoint vrátí data podle ID, ale neověří, že na ně má uživatel právo — změním /orders/123 na 124 a vidím cizí objednávku. Ošetření: na serveru vždy ověř vlastnictví / oprávnění (objednávka patří přihlášenému uživateli), ne jen to, že je přihlášený. Je to jedna z nejčastějších reálných děr.

Proč nestačí hesla hashovat přes SHA-256?

SHA-256 je rychlá a bez soli → útočník zkusí miliardy hesel za sekundu a předpočítané tabulky prolomí běžná hesla okamžitě. Použij bcrypt/argon2/scrypt: jsou záměrně pomalé (zkoušení hesel je drahé) a používají salt (náhodnou přísadu pro každého zvlášť, takže stejné heslo má pokaždé jiný otisk).

Jak se bránit SQL injection?

Parametrizovanými dotazy nebo ORM — vstup se předá jako parametr, ne jako součást textu dotazu, takže ho databáze nikdy nevezme jako příkaz. Nikdy neslepuj dotaz z textu od uživatele. Navíc validuj vstup a dej databázovému účtu jen nezbytná práva (least privilege), aby případný průnik napáchal míň škody.

Co znamená nevěřit ničemu, co přijde od klienta?

Cokoli přijde z prohlížeče nebo mobilu, může být podvržené — validace na frontendu je jen pohodlí pro uživatele, ne bezpečnost. Proto validuj a kontroluj oprávnění na serveru, i když to ověřuje i klient. Schované tlačítko v UI není ochrana; ochranou je serverová kontrola přímo na endpointu.

Jaký je rozdíl mezi RBAC a ABAC?

RBAC (role-based) přiděluje práva přes role — uživatel má roli (admin, editor) a ta určuje, co smí. Jednoduché, stačí na většinu aplikací. ABAC (attribute-based) odvozuje práva z atributů uživatele, zdroje a kontextu („editor smí upravit dokument, jen pokud je z jeho oddělení"). Mocnější, ale složitější. Začni s RBAC, po ABAC sáhni, až pravidla přerostou jednoduché role.

Kam patří tajné údaje (secrets) a co je rotace?

Tajné údaje (API klíče, hesla k databázi) nepatří do kódu ani do gitu — odtud se už pořádně nesmažou. Patří do proměnných prostředí, líp do secret manageru, kde se dají bezpečně uložit, sledovat přístup a rotovat = pravidelně měnit, aby starý uniklý klíč brzy přestal platit. Když nějaký secret unikne, okamžitě ho vyměň.

Je CORS ochrana mého serveru?

Ne. CORS je pravidlo prohlížeče, které chrání uživatele před tím, aby cizí web z jeho prohlížeče volal tvé API a četl odpovědi. Tvůj server neochrání — kdokoli s curlem nebo vlastním backendem ho zavolá bez ohledu na CORS. Proto autentizaci a autorizaci musíš mít na serveru tak jako tak. A nikdy nekombinuj Access-Control-Allow-Origin: * s credentials — povoluj konkrétní originy (whitelist).