Bezpieczeństwo aplikacji (AppSec): OWASP Top 10 w praktyce
Praktyczny przegląd OWASP Top 10: wstrzyknięcia, XSS, CSRF, IDOR, SSRF, deserializacja, supply chain i hashowanie haseł, z kodem i obroną w głąb dla inżynierów.
- Parametryzowane zapytania jako jedyna niezawodna obrona przed SQL Injection
- Kontekstowe enkodowanie wyjścia i strict CSP z nonce przeciw XSS
- CSRF: token + SameSite jako obrona w głąb, także na endpointach przed logowaniem
- Weryfikacja właściciela zasobu przy każdym żądaniu jako obrona przed IDOR
- Argon2id/bcrypt do haseł, nigdy MD5/SHA - zasada least privilege wszędzie
- SSRF: walidacja adresu IP po rezolucji DNS, blokada endpointów metadanych chmury
1 · Wstrzyknięcia: SQL, Command i NoSQL Injection
★ egzaminWstrzyknięcie (injection) figuruje w OWASP Top 10:2025 jako kategoria A05 i pozostaje jednym z najgroźniejszych błędów: dane wejściowe użytkownika trafiają do interpretera (SQL, powłoki systemowej, silnika NoSQL) i zostają wykonane jako kod zamiast być traktowane jako dane. Skutek: pełny odczyt/zapis bazy, zdalne wykonanie kodu, przejęcie serwera.
# NIEBEZPIECZNE: konkatenacja stringów
query = f"SELECT * FROM users WHERE email = '{email}'"
cursor.execute(query) # email = "' OR '1'='1" omija filtr
# BEZPIECZNE: parametryzowane zapytanie, driver escapuje wartość
cursor.execute(
"SELECT * FROM users WHERE email = %s",
(email,),
) import subprocess
# NIEBEZPIECZNE: shell=True + interpolacja
subprocess.run(f"convert {filename} output.png", shell=True)
# BEZPIECZNE: lista argumentów, brak powłoki
subprocess.run(["convert", filename, "output.png"], shell=False, check=True) // NIEBEZPIECZNE: obiekt z request.body trafia bezpośrednio do query
db.users.findOne({ username: req.body.username, password: req.body.password });
// atak: password = { "$ne": null } omija uwierzytelnienie
// BEZPIECZNE: wymuszenie typu string przed budową zapytania
const username = String(req.body.username);
const password = String(req.body.password);
db.users.findOne({ username, password: hash(password) }); - Zawsze używaj parametryzowanych zapytań lub bezpiecznych metod ORM, nigdy raw query ze sklejaniem stringów
- Konto bazodanowe aplikacji ma mieć minimalne uprawnienia (least privilege), nie prawa administratora
- Nazwy kolumn/tabel w ORDER BY, LIMIT i podobnych miejscach: whitelist, nie interpolacja
- Wyłącz niebezpieczne funkcje silnika bazy (np. xp_cmdshell w SQL Server)
- Nigdy nie zwracaj surowego błędu SQL do klienta - ujawnia strukturę bazy
2 · Broken Authentication i zarządzanie sesją
★ egzaminBłędy uwierzytelniania i zarządzania sesją (OWASP A07: Authentication Failures) prowadzą do przejęcia konta bez znajomości hasła: przewidywalne tokeny sesji, brak wygasania, brak rotacji po zmianie uprawnień.
from flask import session, current_app
@app.route("/login", methods=["POST"])
def login():
user = authenticate(request.form["email"], request.form["password"])
if user is None:
abort(401)
session.clear()
current_app.session_interface.regenerate(session) # rotates the actual session ID (Flask-Session); session.clear() alone only empties the payload, it does not fix a pre-login session ID
session["user_id"] = user.id
session["issued_at"] = time.time()
return redirect("/dashboard") SESSION_TTL_SECONDS = 30 * 60
def is_session_valid(session: dict) -> bool:
if "user_id" not in session:
return False
if time.time() - session["issued_at"] > SESSION_TTL_SECONDS:
return False
return session["user_id"] not in revoked_user_ids # server-side revocation list - Wymuszaj MFA dla operacji wrażliwych (ASVS L2)
- Sprawdzaj hasła wobec list wycieków (np. Have I Been Pwned range API)
- Rate-limiting na endpointach logowania, blokada po N nieudanych próbach
- Unieważniaj wszystkie sesje/tokeny po zmianie hasła lub usunięciu z organizacji
- Ciasteczko sesji: HttpOnly, Secure, SameSite - nigdy w localStorage
Sesja stanowa (server-side)
- Łatwa natychmiastowa rewokacja
- Wymaga magazynu (Redis/DB)
- Domyślnie bezpieczniejsza dla wrażliwych operacji
JWT bezstanowy
- Brak zapytania do bazy przy każdej weryfikacji
- Rewokacja wymaga blacklisty/jti + krótkiego TTL
- Ryzyko: alg confusion, przechowywanie w localStorage
3 · XSS: Stored, Reflected, DOM i kontekstowe enkodowanie
★ egzaminXSS (Cross-Site Scripting) pozwala atakującemu wykonać dowolny JavaScript w przeglądarce ofiary w kontekście zaufanej domeny: kradzież ciasteczek sesji, keylogging, podmiana treści, ataki na innych użytkowników.
// BEZPIECZNE: JSX escapuje automatycznie przy renderze
function Comment({ text }: { text: string }) {
return <p>{text}</p>;
}
// NIEBEZPIECZNE: świadome wyłączenie escapowania bez sanitizacji
function CommentUnsafe({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// BEZPIECZNE, gdy HTML jest wymagany: sanitizacja przez DOMPurify
import DOMPurify from "dompurify";
function CommentRichText({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, { ALLOWED_TAGS: ["b", "i", "a"] });
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
} Content-Security-Policy:
script-src 'nonce-{RANDOM_PER_REQUEST}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
frame-ancestors 'none'; | Kontekst wyjścia | Rodzaj enkodowania | Przykład |
|---|---|---|
| HTML body | HTML entity encode | < -> <, > -> > |
| Atrybut HTML | HTML attribute encode + cudzysłów | " -> " |
| JavaScript inline | JS string escape, nigdy wstawianie surowego stringu | ' -> \' |
| URL/query string | URL encode (percent-encoding) | spacja -> %20 |
| CSS | CSS escape | znaki specjalne jako \XX |
- Włącz strict CSP z nonce per-request zamiast allowlisty domen (allowlista jest łamliwa i rozrasta się do setek hostów)
- Nagłówki: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin
- SVG upload może zawierać <script> - traktuj jak HTML, nie jak obraz
- Loguj naruszenia CSP przez report-to/report-uri
4 · CSRF i obrona SameSite
★ egzaminCSRF zmusza przeglądarkę ofiary, zalogowanej w aplikacji, do wysłania niezamierzonego żądania (np. zmiana hasła, przelew): przeglądarka automatycznie dołącza ciasteczka sesji do żądania z dowolnej domeny, więc serwer widzi je jako 'uwierzytelnione'.
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
csrf = CSRFProtect(app) # wymusza token na każdym POST/PUT/PATCH/DELETE
@app.route("/account/email", methods=["POST"])
def change_email():
# token walidowany automatycznie przez CSRFProtect przed wejściem tutaj;
# brak/zły token -> 400 Bad Request
... SameSite=Strict
- Ciasteczko nigdy nie wysyłane cross-site
- Najwyższe bezpieczeństwo
- Może zepsuć nawigację z linków zewnętrznych (np. e-mail -> zalogowana strona)
SameSite=Lax
- Ciasteczko wysyłane przy nawigacji top-level (kliknięcie linku)
- Dobry kompromis, częste zachowanie domyślne
- Nadal wymaga tokenu CSRF dla pełnej ochrony
- SameSite to warstwa dodatkowa, nie zamiennik tokenu CSRF
- Nadmiernie permisywny CORS (Access-Control-Allow-Origin: *) z Allow-Credentials potrafi obejść SameSite
- Nie wstawiaj tokenu CSRF do URL - używaj nagłówka, np. X-CSRF-Token
- Regeneruj token po zmianie stanu uwierzytelnienia
5 · Niebezpieczna deserializacja
Deserializacja niezaufanych danych pozwala atakującemu skonstruować obiekt, którego proces odtwarzania (deserializacji) wykonuje dowolny kod, zanim aplikacja zdąży cokolwiek zwalidować. Klasyczny przykład: pickle w Pythonie, ObjectInputStream w Javie, unserialize() w PHP.
import pickle, json
# NIEBEZPIECZNE: deserializacja niezaufanych danych wykonuje dowolny kod
data = pickle.loads(untrusted_bytes_from_request)
# BEZPIECZNE: format danych bez wykonywalnego kodu
data = json.loads(untrusted_bytes_from_request) ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.app.model.*;!*" // dozwolone tylko klasy z com.app.model, reszta odrzucona
);
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filter); - Preferuj formaty bez wykonywalnego kodu: JSON, Protocol Buffers, MessagePack
- Jeśli deserializacja obiektów jest konieczna: podpisuj serializowane dane (HMAC) i weryfikuj podpis przed deserializacją
- Whitelisting dozwolonych klas/typów zamiast blacklisty
- Izoluj proces deserializujący (sandbox, brak uprawnień sieciowych/systemowych)
6 · Security Misconfiguration i SSRF
★ egzaminSecurity Misconfiguration (OWASP A02) to najczęstsza kategoria luk w praktyce: domyślne hasła, włączony debug w produkcji, niepotrzebne usługi, brak nagłówków bezpieczeństwa. SSRF (Server-Side Request Forgery) to jej niebezpieczny szczególny przypadek: serwer wykonuje żądanie HTTP do adresu kontrolowanego przez atakującego.
import ipaddress
import socket
from urllib.parse import urlparse
def is_safe_url(url: str) -> bool:
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
return False
ip = socket.gethostbyname(parsed.hostname) # rozwiąż DNS PRZED walidacją
addr = ipaddress.ip_address(ip)
if addr.is_private or addr.is_loopback or addr.is_link_local:
return False # blokuje też 169.254.169.254 (metadata endpoint)
return True server {
autoindex off; # brak listowania katalogów
server_tokens off; # ukryj wersję Nginx w nagłówku Server
location /admin { deny all; } # panel admina niedostępny z zewnątrz
} | Błąd konfiguracji | Konsekwencja | Poprawka |
|---|---|---|
| Tryb debug włączony w produkcji | Stack trace ujawnia ścieżki, wersje, czasem sekrety | DEBUG=False w produkcji, generyczne strony błędów |
| Domyślne konto/hasło admina | Natychmiastowy dostęp administracyjny | Wymuszona zmiana hasła przy pierwszym uruchomieniu |
| Nadmiarowe uprawnienia CORS (*) | Dowolna domena czyta odpowiedzi API | Allowlist konkretnych originów |
| Brak limitu przekierowań w kliencie HTTP | SSRF przez łańcuch redirectów do zasobu wewnętrznego | Limit/walidacja każdego przeskoku redirecta |
- Allowlist domen docelowych zamiast blacklisty adresów wewnętrznych
- Ogranicz timeout i rozmiar odpowiedzi przy żądaniach wychodzących
- Segmentacja sieci: serwis pobierający URL-e działa bez dostępu do sieci wewnętrznej
- Regularny przegląd konfiguracji przez checklisty (CIS Benchmarks, OWASP ASVS)
7 · Broken Access Control (IDOR) i zasada najmniejszych uprawnień
★ egzaminBroken Access Control to od lat #1 kategoria OWASP Top 10 pod względem częstości występowania. IDOR (Insecure Direct Object Reference) to jego najczęstszy podtyp: aplikacja ufa identyfikatorowi zasobu podanemu przez klienta, zamiast weryfikować, czy zalogowany użytkownik ma prawo do tego zasobu.
@app.route("/api/invoices/<invoice_id>")
@login_required
def get_invoice(invoice_id):
invoice = db.get_invoice(invoice_id)
if invoice is None:
abort(404) # nie ujawniaj, czy zasób istnieje
if invoice.owner_id != current_user.id and not current_user.is_admin:
abort(404) # 404, nie 403 - utrudnia enumerację zasobów
return jsonify(invoice.to_dict()) -- Konto aplikacji: tylko operacje na danych, zero uprawnień strukturalnych
CREATE ROLE app_user LOGIN PASSWORD '...';
GRANT SELECT, INSERT, UPDATE, DELETE ON orders, invoices TO app_user;
REVOKE ALL ON SCHEMA public FROM app_user; -- brak DROP/ALTER/CREATE - Weryfikuj właściciela zasobu przy KAŻDYM żądaniu, nie polegaj na tym, że UI 'nie pokazuje' cudzych danych
- Sprawdzaj też zasoby nadrzędne (np. komentarz -> post -> właściciel posta)
- Re-waliduj uprawnienia po każdej zmianie roli/organizacji
- Konta serwisowe i klucze API: osobne, wąskie zakresy (scopes) zamiast jednego klucza z pełnym dostępem
8 · Supply chain, zarządzanie sekretami i hashowanie haseł
★ egzaminNowoczesna aplikacja to w 80-90% kod cudzy: zależności npm/pip/cargo. OWASP A03:2025 (Software Supply Chain Failures) i A04 (Cryptographic Failures) obejmują ryzyko podatnych zależności oraz błędy w przechowywaniu sekretów i haseł - trzy różne problemy, jedna przyczyna: ślepe zaufanie do tego, co 'zawsze tak było'.
import hashlib
from argon2 import PasswordHasher
# NIEBEZPIECZNE: szybki hash bez soli, łamany tęczowymi tablicami w sekundy
hashlib.md5(password.encode()).hexdigest()
# BEZPIECZNE: Argon2id, sól generowana automatycznie, koszt pamięciowy wbudowany
ph = PasswordHasher()
hashed = ph.hash(password) # zapisz `hashed` do bazy
ph.verify(hashed, password_attempt) # rzuca wyjątkiem przy niezgodności # Zamroź dokładne wersje przechodnich zależności (powtarzalny build)
pip freeze > requirements.lock.txt
# Skanuj znane CVE w zainstalowanych pakietach
pip install pip-audit && pip-audit # .env (w .gitignore, NIGDY commitowane)
DATABASE_URL=postgresql://user:pass@host:5432/db
JWT_SECRET=<losowe 256+ bitów> import os
database_url = os.environ["DATABASE_URL"] - Nigdy nie commituj sekretów do repozytorium (nawet w historii - rotacja po wycieku obowiązkowa)
- NEXT_PUBLIC_*/REACT_APP_* trafiają do bundla klienta - żadnych kluczy prywatnych pod tymi prefiksami
- Generuj i utrzymuj SBOM (np. przez cyclonedx/syft) dla każdego wydania
- Automatyczne skanowanie zależności w CI (Dependabot, pip-audit, npm audit) blokujące merge przy krytycznych CVE
- Least privilege także dla kluczy API: osobny klucz o wąskim zakresie na integrację, nie jeden klucz-wytrych
Sprawdź się - testowanie to nauka
24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.