OWASP Top 10 — 2021
OWASP (Open Web Application Security Project) publikuje Top 10 nejkritičtějších bezpečnostních rizik. Je to mezinárodně uznávaný standard pro bezpečnost webových aplikací.
Broken Access Control
Uživatel přistupuje ke zdrojům, na které nemá právo. IDOR, přímý přístup k URL admin panelu bez ověření role.
Cryptographic Failures
Plaintext hesla, MD5/SHA1 pro hesla, HTTP místo HTTPS, nešifrované citlivé data v DB.
Injection (XSS, SQL, Command)
Nevalidovaný user input předaný do interpreta. SQL injection, XSS, Command injection.
Insecure Design
Chybějící threat modeling, bezpečnostní požadavky nebyly zvažovány v návrhu.
Security Misconfiguration
Default credentials, verbose error messages v produkci, otevřené cloud storage buckety, chybějící security headers.
Vulnerable & Outdated Components
npm závislosti se známými CVE, neaktualizovaný OS nebo framework.
Identification & Auth Failures
Slabá hesla, chybějící MFA, insecure "zapomenuté heslo", brute-force bez rate limitingu.
Software & Data Integrity Failures
Insecure deserialization, nekontrolované npm závislosti, CI/CD bez ověření integrity.
Security Logging & Monitoring Failures
Nepozorované breaches, chybějící logování přihlášení, alerting neexistuje.
Server-Side Request Forgery (SSRF)
Server fetchuje URL z user input — útočník přistupuje k interní síti, AWS metadata, atd.
XSS — Cross-Site Scripting
XSS je injekce škodlivého JavaScript kódu do stránky. Útočník může ukrást session tokeny, přesměrovat uživatele, nebo ovládnout prohlížeč.
Typy XSS
| Typ | Popis | Příklad |
|---|---|---|
| Stored XSS | Payload uložen v DB, zobrazen všem | Komentář s <script> |
| Reflected XSS | Payload v URL, zobrazen okamžitě | ?q=<script>alert(1)</script> |
| DOM XSS | Payload zpracován JS bez server roundtrip | innerHTML = location.hash |
// Uložení komentáře do DB
const comment = req.body.comment;
// "Skvělý produkt!
// "
// Renderování bez escape
// ❌ innerHTML = nebezpečné!
div.innerHTML = comment;
// ❌ React dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{
__html: userInput
}} />
// Výsledek: útočník ukradne
// session cookie všech čtenářů!
// 1. Escape při výstupu
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
// 2. textContent místo innerHTML
el.textContent = userInput; // ✅
// 3. DOMPurify pro rich text
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirty);
el.innerHTML = clean;
// 4. Content Security Policy header
// Zablokuje inline skripty!
{}. Nebezpečné je pouze dangerouslySetInnerHTML — používejte ho jen s DOMPurify sanitizací pro rich text (blog content, markdown).// Kvíz: Útočník vloží do komentářového pole: <img src=x onerror="stealCookies()">. Jaký typ XSS je to?
SQL Injection
SQL injection je vložení SQL kódu do dotazu prostřednictvím user input. Útočník může číst, modifikovat nebo smazat celou databázi.
// Přihlášení s SQL injection
const email = "admin'--";
// nebo: ' OR '1'='1
const query = `
SELECT * FROM users
WHERE email = '${email}'
AND password = '${password}'
`;
// Výsledný dotaz:
// SELECT * FROM users
// WHERE email = 'admin'--'
// AND password = '...'
// Komentář (--) ignoruje heslo!
// Útočník se přihlásí jako admin
// Ještě horší:
const id = "1; DROP TABLE users;--";
// Smaže celou tabulku!
// ✅ Parametrizované dotazy (pg)
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[email] // ← odděleně, NIKDY concat
);
// ✅ Prisma ORM (automaticky safe)
const user = await prisma.user.findUnique({
where: { email } // Prisma parametrizuje
});
// ✅ mysql2 prepared statements
const [rows] = await conn.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
// NIKDY string interpolace
// s user inputem do SQL!
CSRF — Cross-Site Request Forgery
CSRF přinutí přihlášeného uživatele provést nechtěnou akci. Útočník vytvoří stránku, která v pozadí pošle request na váš server — s cookies uživatele.
// CSRF útok: Stránka evil.com obsahuje:
<img src="https://banka.cz/prevod?na=utocnik&castka=50000" />
// Pokud je uživatel přihlášen na banka.cz, cookie se pošle!
// CSRF OBRANA 1: SameSite cookie
res.cookie('sessionId', token, {
httpOnly: true, // JS nemůže číst cookie
secure: true, // Jen HTTPS
sameSite: 'strict', // NIKDY nepošle přes cross-site request ✅
// nebo 'lax' — pošle při top-level navigaci (link kliknutí)
});
// CSRF OBRANA 2: CSRF token (pro starší browsery)
// Server vygeneruje náhodný token → uloží do session
// Klient ho přidá do každého POST requestu
// Server verifikuje shodu
// Express + csrf middleware
import csrf from 'csurf';
app.use(csrf({ cookie: true }));
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
// <input type="hidden" name="_csrf" value="{{ csrfToken }}">
// CSRF OBRANA 3: Custom request header (fetch API)
// Cross-origin requesty nemohou přidávat custom headery!
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest', // CSRF check
},
body: JSON.stringify({ amount: 100 }),
});
// Server: pokud chybí X-Requested-With → odmítnout
// Kvíz: Proč SameSite: 'strict' cookie chrání před CSRF?
Security Headers
HTTP bezpečnostní hlavičky instruují prohlížeč, jak chránit stránku. Jednoduché nastavení — velký bezpečnostní dopad.
// Next.js — next.config.js
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
// Vynucuje HTTPS po dobu 1 roku
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN' // Zabrání clickjacking útokům (iframe)
},
{
key: 'X-Content-Type-Options',
value: 'nosniff' // Zabrání MIME-type sniffing
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
},
{
// Content Security Policy — nejsilnější zbraň proti XSS
key: 'Content-Security-Policy',
value: [
"default-src 'self'", // Jen vlastní doména
"script-src 'self' 'nonce-{NONCE}'", // JS jen z vlastní domény + nonce
"style-src 'self' fonts.googleapis.com", // CSS + Google Fonts
"font-src 'self' fonts.gstatic.com", // Fonty
"img-src 'self' data: https:", // Obrázky
"connect-src 'self' https://api.myapp.com", // fetch/XHR
"frame-ancestors 'none'", // Zakáže iframe embedding
"upgrade-insecure-requests", // HTTP → HTTPS auto
].join('; ')
},
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
},
};
// Express — s helmet middleware
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-abc123'"],
styleSrc: ["'self'", "fonts.googleapis.com"],
},
},
}));
Secrets & Environment Variables
Nejčastější bezpečnostní chyba junior vývojářů — API klíče přímo v kódu nebo v Gitu. Důsledky: kompromitace, finanční škoda, GDPR pokuty.
// ❌ NIKDY takto — klíče v kódu
const stripeKey = 'sk_live_XXXXXXXXXXXX';
fetch('https://api.openai.com', {
headers: { Authorization: 'Bearer sk-proj-XXXX' }
});
// ❌ NIKDY v frontend bundle (Vite bez VITE_ prefix)
const dbPassword = import.meta.env.DB_PASSWORD; // Viditelné v browser devtools!
// ✅ Server-side env (Node.js / Next.js server)
const stripeKey = process.env.STRIPE_SECRET_KEY;
const dbUrl = process.env.DATABASE_URL;
// ✅ Vite frontend — jen VITE_ prefix (viditelné v bundlu!)
const apiUrl = import.meta.env.VITE_API_URL; // OK — není secret
// NIKDY: VITE_STRIPE_SECRET nebo VITE_DATABASE_URL!
// ✅ Validace env vars při startu (fail-fast)
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.coerce.number().default(4000),
});
// Selže hned při startu pokud chybí klíče
export const env = envSchema.parse(process.env);
// .env.example (DO GITU — bez hodnot!)
// DATABASE_URL=postgresql://user:pass@host:5432/db
// JWT_SECRET=minimum-32-characters-random-string
// STRIPE_SECRET_KEY=sk_live_...
// .gitignore — povinné!
// .env
// .env.local
// .env.production
git-secrets jako pre-commit hook.// Kvíz: Proč je nebezpečné uložit Stripe Secret Key do Vite env var s prefixem VITE_?
Security Audit & Nástroje
# npm audit — kontrola závislostí na CVE
$ npm audit
# found 3 vulnerabilities (1 critical, 2 high)
$ npm audit fix # auto-fix kde je to bezpečné
$ npm audit fix --force # force update (opatrně!)
# Pravidelně aktualizovat závislosti
$ npx npm-check-updates -u && npm install
# SAST nástroje (Static Application Security Testing)
# ESLint plugin pro bezpečnost:
# npm install -D eslint-plugin-security
# Snyk — bezpečnostní skener
$ npx snyk test
$ npx snyk monitor # průběžné sledování
# OWASP ZAP — dynamické testování (DAST)
# docker run -t owasp/zap2docker-stable zap-baseline.py -t https://mujweb.cz
# Kontroly v CI/CD
# .github/workflows/security.yml
name: Security
on: [push]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm audit --audit-level=high
- run: npx snyk test --severity-threshold=high
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
Interaktivní Security Checklist
Zaškrtněte bezpečnostní prvky, které jste implementovali ve svém projektu. Sledujte svůj security score.
// Bezpečnostní audit — zaškrtněte co máte
Zaškrtněte implementované prvky →
Security Engineer
Zvládáte OWASP Top 10, XSS, SQL injection, CSRF, security headers a správu secrets.
// Taháček
// XSS obrana
el.textContent = userInput; // ✅ bezpečné
el.innerHTML = DOMPurify.sanitize(richText); // ✅ sanitized
// el.innerHTML = userInput; // ❌ NIKDY
// SQL injection
await db.query('SELECT * WHERE id = $1', [id]); // ✅
// `SELECT * WHERE id = ${id}` // ❌ NIKDY
// CSRF
res.cookie('token', val, {
httpOnly: true, secure: true, sameSite: 'strict'
});
// Security headers (Express)
app.use(helmet());
// CSP key directives
"default-src 'self'; script-src 'self'; frame-ancestors 'none'"
// Env vars
const env = envSchema.parse(process.env); // validace při startu
// NIKDY VITE_SECRET_KEY — viditelné v bundle!
// npm audit
npm audit --audit-level=high
npx snyk test