Bezpieczeństwo aplikacji (AppSec): OWASP Top 10 w praktyce
Przegląd OWASP Top 10: wstrzyknięcia, XSS, CSRF, IDOR, SSRF, deserializacja, supply chain i hashowanie haseł, z kodem, dla inżynierów.
Bezpieczeństwo aplikacji (AppSec): OWASP Top 10 w praktyce
Przegląd OWASP Top 10: wstrzyknięcia, XSS, CSRF, IDOR, SSRF, deserializacja, supply chain i hashowanie haseł, z kodem, dla inżynierów.
Wstrzyknięcia: SQL, Command i NoSQL Injection
na egzaminieWstrzyknięcie (injection) to kategoria A05 w OWASP Top 10:2025 i jeden z najgroźniejszych błędów. Dane użytkownika trafiają do interpretera (SQL, powłoki systemowej, silnika NoSQL) i wykonują się jako kod, nie jako dane.
Skutek: pełny odczyt/zapis bazy, zdalne wykonanie kodu, przejęcie serwera.
Parametryzowane zapytania (prepared statements) to jedyna niezawodna obrona przy SQL. Walidacja wejścia to dodatkowa warstwa, nie zamiennik.
SQL Injection: Wstrzyknięcie fragmentu SQL przez niesparametryzowane dane wejściowe, pozwalające zmienić logikę zapytania (np. ominąć WHERE, dołączyć UNION SELECT, wywołać podzapytania administracyjne).
Command Injection: Wstrzyknięcie poleceń powłoki systemowej przez dane trafiające do funkcji uruchamiających procesy (os.system, exec, subprocess z shell=True). Dzieje się tak, gdy polecenie jest budowane jako string zamiast listy argumentów.
NoSQL Injection: Wstrzyknięcie operatorów zapytań (np. $where, $ne, $gt w MongoDB) przez niekontrolowane obiekty JSON, pozwalające ominąć uwierzytelnienie lub wykonać dowolny JavaScript po stronie bazy.
Częsty błąd: deweloper 'sparametryzował' zapytanie, ale kolumnę w ORDER BY lub nazwę tabeli wstawia przez f-string, bo 'parametrów tam nie da się użyć'. To wciąż SQL injection w innym miejscu zapytania. Rozwiązanie: whitelisting dozwolonych nazw kolumn/tabel, nigdy interpolacja.
# 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
Dane to dane, kod to kod: user input nigdy nie powinien zmieniać struktury zapytania, tylko jego wartości.
Broken Authentication i zarządzanie sesją
na egzaminieBłędy uwierzytelniania i zarządzania sesją (OWASP A07: Authentication Failures) prowadzą do przejęcia konta bez znajomości hasła. Przyczyny to przewidywalne tokeny sesji, brak wygasania i brak rotacji po zmianie uprawnień.
Token sesji wymaga min. 128 bitów entropii, generowany kryptograficznie bezpiecznym generatorem liczb losowych, nigdy z Math.random() ani sekwencyjnego licznika.
Session Fixation: Atakujący narzuca ofierze znany identyfikator sesji (np. przez link z ?sessionid=). Po zalogowaniu ofiary przejmuje tę samą sesję, bo aplikacja nie regeneruje ID po uwierzytelnieniu.
Broken Session Management: Sesje, które nie wygasają, nie są unieważniane po wylogowaniu, lub pozostają aktywne po zmianie hasła czy uprawnień użytkownika.
Częsty błąd: token sesji jest unieważniany tylko po stronie klienta (usunięcie ciasteczka), a serwer nadal go akceptuje. Atakujący z przechwyconym tokenem ma dostęp mimo 'wylogowania' ofiary.
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
Nowa sesja przy każdej zmianie stanu uprzywilejowania: logowanie, zmiana hasła, zmiana roli - zawsze session.clear() lub rotacja tokenu.
XSS: Stored, Reflected, DOM i kontekstowe enkodowanie
na egzaminieXSS (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.
Stored: payload zapisany w bazie i serwowany innym użytkownikom. Reflected: payload w parametrze URL odbity natychmiast w odpowiedzi. DOM-based: payload nigdy nie trafia na serwer, wykonuje się przez manipulację DOM po stronie klienta (np. innerHTML z location.hash).
Stored XSS: Złośliwy skrypt trwale zapisany w bazie danych (np. w komentarzu, opisie profilu) i renderowany bez enkodowania każdemu użytkownikowi odwiedzającemu stronę. To najgroźniejszy wariant, bo skaluje się automatycznie.
DOM-based XSS: Podatność istnieje wyłącznie w kodzie JavaScript po stronie klienta. Dane ze źródła kontrolowanego przez atakującego (location.hash, document.referrer, postMessage) trafiają do zlewu (innerHTML, eval, document.write) bez przejścia przez serwer.
Częsty błąd: developerzy sanitują dane przy zapisie do bazy, ale ten sam string jest renderowany w trzech różnych kontekstach (HTML, atrybut, JavaScript inline). Enkodowanie musi być kontekstowe, przy wyjściu (output encoding), nie jednorazowe przy wejściu.
// 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
Escapuj przy wyjściu, w kontekście, w którym dane wylądują - nie raz przy wejściu na wszelki wypadek.
CSRF i obrona SameSite
na egzaminieCSRF 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'.
Każdy endpoint zmieniający stan (POST/PUT/PATCH/DELETE oraz każdy GET, który zmienia stan - co jest błędem samo w sobie) wymaga ochrony CSRF. Dotyczy to też endpointów przed logowaniem: login, rejestracja, reset hasła.
Login CSRF: Atakujący wymusza zalogowanie ofiary na konto kontrolowane przez atakującego, np. by śledzić późniejszą aktywność ofiary w kontekście cudzego konta. Ten atak jest często pomijany, bo 'to przecież nie zmienia danych ofiary'.
Double-submit cookie: Wzorzec obrony: token CSRF wysyłany jednocześnie w ciasteczku i w nagłówku/body żądania. Serwer odrzuca żądanie, jeśli wartości się nie zgadzają. Działa bez serwerowego magazynu tokenów.
Częsty błąd: walidacja tokenu CSRF jest 'opcjonalna', gdy token jest pusty (np. stare klienty API) - brak tokenu musi zawsze oznaczać odrzucenie żądania, nigdy przepuszczenie.
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
- SameSite nie chroni przed żądaniem z tej samej witryny (np. z przejętej subdomeny). CORS odbijający dowolny Origin z Allow-Credentials pozwala obcej stronie czytać odpowiedzi z ciasteczkiem SameSite=None
- Nie wstawiaj tokenu CSRF do URL - używaj nagłówka, np. X-CSRF-Token
- Regeneruj token po zmianie stanu uwierzytelnienia
Brak tokenu = odrzucone żądanie, zawsze, bez wyjątków 'na razie'.
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.
W przeciwieństwie do XSS czy SQLi, atak trafia zanim dane trafią w ręce logiki biznesowej - często wystarczy jeden request HTTP ze złośliwym payloadem serializowanym.
Gadget chain: Łańcuch istniejących w aplikacji klas, których metody (np. __reduce__, readObject) wywołują się automatycznie przy deserializacji. Atakujący składa je tak, by ich efekt uboczny uruchomił dowolny kod.
Częsty błąd: deweloper używa pickle.loads() do 'wygodnego' cache'owania obiektów Pythona w Redis/na dysku. Zapomina, że Redis/dysk może być zapisywalny przez inny, mniej zaufany komponent systemu. Pickle nie ma trybu bezpiecznego dla danych z zewnątrz.
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)
Ataki na deserializację często wykorzystują 'gadgety' z bibliotek trzecich obecnych w classpath/site-packages - kolejny powód do audytu zależności.
Jeśli format danych umie wywołać kod przy odczycie, traktuj go jak kod, nie jak dane.
Security Misconfiguration i SSRF
na egzaminieSecurity 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.
W Top 10:2025 SSRF włączono do kategorii A01 (Broken Access Control) jako przypadek łamania granic zaufania serwera. W praktyce traktuje się go jednak jako odrębną klasę podatności, ze względu na specyficzne wektory (webhooki, generatory PDF, import z URL).
SSRF: Podatność, w której serwer aplikacji wykonuje żądanie sieciowe pod adres URL dostarczony lub wpływany przez użytkownika. Pozwala to dosięgnąć zasoby wewnętrzne (metadata API chmury, bazy danych, panele admina) niedostępne z zewnątrz.
Cloud metadata endpoint: Wewnętrzny adres (np. 169.254.169.254) udostępniający tymczasowe poświadczenia instancji chmurowej - klasyczny cel SSRF, bo zwraca klucze IAM/role bez uwierzytelnienia.
Częsty błąd: filtr URL blokuje 'localhost' i '127.0.0.1' jako stringi, ale nie łapie zapisu dziesiętnego (2130706433) ani oktalnego (0177.0.0.1). Walidacja musi się odbywać po rozwiązaniu DNS, na faktycznym adresie IP.
Samo sprawdzenie nie zatrzyma DNS rebinding: jeśli klient HTTP rozwiąże nazwę jeszcze raz, dostanie inny adres niż sprawdzony. Żądanie musi pójść pod ten sam, sprawdzony adres IP.
import ipaddress
import socket
from urllib.parse import urlparse
def resolve_safe_ip(url: str) -> str | None:
parsed = urlparse(url)
if parsed.scheme not in ("http", "https") or not parsed.hostname:
return None
# rozwiąż DNS PRZED walidacją i sprawdź KAŻDY adres (IPv4 i IPv6)
infos = socket.getaddrinfo(parsed.hostname, None)
addrs = [ipaddress.ip_address(info[4][0]) for info in infos]
if not all(addr.is_global for addr in addrs):
return None # blokuje też 169.254.169.254 (metadata endpoint)
# Łącz się pod TEN adres (nagłówek Host = parsed.hostname) i nie podążaj
# za przekierowaniem bez ponownej walidacji: drugie rozwiązanie nazwy
# przez klienta HTTP otwiera drogę dla DNS rebinding.
return str(addrs[0]) 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)
Zaufanie do URL kończy się na scheme http/https - adres IP zawsze sprawdzaj po rezolucji, nie po stringu.
Broken Access Control (IDOR) i zasada najmniejszych uprawnień
na egzaminieBroken Access Control to od lat kategoria #1 w OWASP Top 10 pod względem częstości występowania. IDOR (Insecure Direct Object Reference) to jego najczęstszy podtyp.
W IDOR aplikacja ufa identyfikatorowi zasobu podanemu przez klienta, zamiast sprawdzić, czy zalogowany użytkownik ma prawo do tego zasobu.
Autoryzacja weryfikowana na poziomie danych (per-request), nie tylko na poziomie routingu - middleware chroniący ścieżkę /admin nie chroni przed dostępem do cudzego zasobu pod /api/orders/{id}.
IDOR: Bezpośrednie odwołanie do obiektu (np. /api/invoices/1042) bez weryfikacji, czy zalogowany użytkownik jest właścicielem zasobu o tym identyfikatorze - zmiana ID w URL ujawnia cudze dane.
Horizontal vs Vertical Privilege Escalation: Eskalacja pozioma: użytkownik A uzyskuje dostęp do danych użytkownika B na tym samym poziomie uprawnień. Eskalacja pionowa: zwykły użytkownik uzyskuje funkcje administratora.
Least Privilege: Każdy komponent (konto bazodanowe, rola użytkownika, klucz API, proces) otrzymuje wyłącznie minimalny zestaw uprawnień niezbędny do realizacji swojej funkcji, nic więcej 'na zapas'.
Częsty błąd: sekwencyjne ID (1, 2, 3...) w publicznym API ułatwiają enumerację zasobów. Nawet z poprawną autoryzacją to informacja wywiadowcza (liczba zamówień, tempo wzrostu firmy). UUIDv4 to obrona w głąb, nie zamiennik autoryzacji.
@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
ID w URL to wskazówka klienta, nie dowód uprawnień - serwer sprawdza właściciela zawsze, niezależnie od tego, co przyszło z frontendu.
Supply chain, zarządzanie sekretami i hashowanie haseł
na egzaminieNowoczesna 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ł.
Te trzy różne problemy (podatne zależności, sekrety, hasła) mają jedną przyczynę: ślepe zaufanie do tego, co 'zawsze tak było'.
Software Bill of Materials to pełna, maszynowo czytelna lista zależności (bezpośrednich i przechodnich) wraz z wersjami. To podstawa do automatycznego wykrycia, czy dana CVE dotyczy Twojej aplikacji, zanim ktoś zapyta ręcznie.
Dependency confusion: Atak, w którym pakiet z publicznego rejestru, o tej samej nazwie i wyższym numerze wersji, podszywa się pod prywatny pakiet firmowy. Menedżer pakietów instaluje wersję atakującego, jeśli konfiguracja nie wymusza źródła prywatnego.
Argon2id: Argon2 (zwycięzca Password Hashing Competition 2015). Jego wariant Argon2id rekomenduje dziś OWASP/RFC 9106 do hashowania haseł. Jest odporny na ataki GPU/ASIC dzięki kosztowi pamięciowemu (memory-hard). Razem z bcrypt i scrypt to jedyny akceptowalny wybór do przechowywania haseł.
Częsty błąd: 'hashujemy hasła, więc jest bezpiecznie'. MD5 i SHA-256 to funkcje szybkie (miliardy prób/sekundę na GPU), zaprojektowane do integralności danych, nie do haseł. Do haseł służą tylko funkcje celowo wolne i pamięciochłonne: Argon2id, bcrypt, scrypt.
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
Hasło hashuj wolno i z pamięcią (Argon2id/bcrypt), zależność sprawdzaj zanim zaufasz, sekret trzymaj poza kodem, zawsze.
Sprawdź się
20 z 24 pytań, losowo, z wyjaśnieniem po każdym. Wynik zostaje na tym urządzeniu.
Rozpocznij quiz