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í.

A01

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.

A02

Cryptographic Failures

Plaintext hesla, MD5/SHA1 pro hesla, HTTP místo HTTPS, nešifrované citlivé data v DB.

A03

Injection (XSS, SQL, Command)

Nevalidovaný user input předaný do interpreta. SQL injection, XSS, Command injection.

A04

Insecure Design

Chybějící threat modeling, bezpečnostní požadavky nebyly zvažovány v návrhu.

A05

Security Misconfiguration

Default credentials, verbose error messages v produkci, otevřené cloud storage buckety, chybějící security headers.

A06

Vulnerable & Outdated Components

npm závislosti se známými CVE, neaktualizovaný OS nebo framework.

A07

Identification & Auth Failures

Slabá hesla, chybějící MFA, insecure "zapomenuté heslo", brute-force bez rate limitingu.

A08

Software & Data Integrity Failures

Insecure deserialization, nekontrolované npm závislosti, CI/CD bez ověření integrity.

A09

Security Logging & Monitoring Failures

Nepozorované breaches, chybějící logování přihlášení, alerting neexistuje.

A10

Server-Side Request Forgery (SSRF)

Server fetchuje URL z user input — útočník přistupuje k interní síti, AWS metadata, atd.

// 01 / 09XSS →

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

TypPopisPříklad
Stored XSSPayload uložen v DB, zobrazen všemKomentář s <script>
Reflected XSSPayload v URL, zobrazen okamžitě?q=<script>alert(1)</script>
DOM XSSPayload zpracován JS bez server roundtripinnerHTML = location.hash
❌ Útok — nebezpečný kód
// 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ářů!
✅ Obrana — správný přístup
// 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!
🛡️
React je XSS-safe by default — JSX automaticky escapuje výrazy v {}. 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.

❌ Útok — string concatenation
// 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!
✅ Obrana — parametrizované dotazy
// ✅ 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!
🔒
Pravidlo č. 1: Nikdy nevkládejte user input přímo do SQL dotazu. Vždy používejte parametrizované dotazy nebo ORM (Prisma, TypeORM). Validujte typy vstupů (číslo je číslo, ne string). Least privilege — DB uživatel by měl mít jen nezbytná oprávnění.
// 03 / 09CSRF →

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"],
    },
  },
}));
// 05 / 09Secrets →

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
🚨
Klíč v Gitu = kompromitace! Pokud omylem pushujete secret do GitHub — okamžitě ho zneplatněte a vygenerujte nový. I po smazání commitu je klíč v historii. GitHub Secret Scanning vás upozorní, ale útočníci jsou rychlejší. Používejte 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 }}
// 07 / 09Cvičení →

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

0%

Zaškrtněte implementované prvky →

🏅

Security Engineer

Zvládáte OWASP Top 10, XSS, SQL injection, CSRF, security headers a správu secrets.

// 08 / 09Taháček →

// 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
// Lekce 10 dokončenaL11: Performance →