Najczęstsze zagrożenia w aplikacjach webowych
Według organizacji OWASP (Open Web Application Security Project), najczęstsze ataki to:
- SQL Injection – wstrzyknięcie kodu SQL poprzez dane wejściowe użytkownika.
- XSS (Cross-Site Scripting) – wstrzyknięcie złośliwego kodu JavaScript do aplikacji.
- CSRF (Cross-Site Request Forgery) – nieautoryzowane żądania wykonywane w imieniu zalogowanego użytkownika.
- Brute-force – łamanie haseł metodą prób i błędów.
- Insecure Deserialization – odczytanie niebezpiecznych danych z sesji.
- Exposure of Sensitive Data – ujawnienie danych użytkowników (hasła, tokeny, dane osobowe).
Każdy z tych ataków może skutkować utratą danych, dostępem do konta admina, a nawet przejęciem całego serwera. OWASP publikuje co roku listę Top 10 największych zagrożeń bezpieczeństwa aplikacji webowych, która jest świetnym punktem wyjścia do nauki o bezpieczeństwie.
SQL Injection – jak się przed tym bronić
SQL injection to klasyczny atak polegający na wstrzyknięciu kodu SQL przez dane wejściowe użytkownika. Atakujący próbuje zmodyfikować zapytanie SQL tak, aby wykonać nieautoryzowane operacje na bazie danych.
Przykład błędnego kodu SQL Injection
Python1 2 3 4 5# NIE RÓB TAK - to jest niebezpieczne! (SQL Injection) username = request.form['username'] password = request.form['password'] query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'" cursor.execute(query)
Atakujący może wstawić coś takiego w pole username:
SQL1admin' OR 1=1; --
To spowoduje, że zapytanie stanie się:
SQL1SELECT * FROM users WHERE username = 'admin' OR 1=1; --' AND password = '...'
Dzięki OR 1=1 (które zawsze jest prawdziwe) i -- (komentarz SQL), atakujący może uzyskać dostęp do całej bazy danych, pomijając sprawdzanie hasła.
Bezpieczne rozwiązanie – użyj ORM
Zawsze używaj ORM (np. Django ORM, SQLAlchemy), które automatycznie parametryzują zapytania:
W Django:
Python1 2 3 4 5 6from myapp.models import User user = User.objects.filter(username=username).first() if user and user.check_password(password): # Logowanie pass
Django ORM automatycznie parametryzuje wszystkie zapytania, co eliminuje ryzyko SQL injection.
W SQLAlchemy (Flask/FastAPI):
Python1 2 3from sqlalchemy.orm import Session user = db.session.query(User).filter(User.username == username).first()
Bezpieczne rozwiązanie – parametryzowane zapytania
Jeśli musisz użyć surowego SQL, zawsze używaj parametrów:
Python1 2 3 4 5# Bezpieczne - parametry są automatycznie escapowane cursor.execute("SELECT * FROM users WHERE username=%s AND password=%s", (username, password)) # Lub w SQLAlchemy db.session.execute(text("SELECT * FROM users WHERE username=:username"), {"username": username})
Zawsze używaj parametrów zapytań zamiast formatowania stringów. Django ORM i SQLAlchemy automatycznie parametryzują zapytania, co eliminuje ryzyko wstrzyknięcia.
XSS – Cross-Site Scripting
Atak XSS pozwala wstrzyknąć złośliwy kod JavaScript, który wykona się w przeglądarce użytkownika. Zazwyczaj dzieje się to, gdy dane wejściowe nie są odpowiednio filtrowane przed wyświetleniem.
Przykład podatności XSS
Python1 2 3# NIE RÓB TAK - to jest niebezpieczne! (XSS) username = request.GET.get('username', '') return f"<p>Witaj {username}</p>"
Jeśli ktoś wpisze w URL:
JavaScript1?username=<script>alert('xss')</script>
to kod JavaScript zostanie wykonany w przeglądarce użytkownika. Atakujący może wykraść cookies, wykonać akcje w imieniu użytkownika lub przekierować go na złośliwą stronę.
Ochrona – escaping HTML
Używaj escapingu HTML – Django robi to automatycznie w szablonach:
W Django:
DJANGO1 2 3 4 5{# Django automatycznie escapuje zmienne #} <p>Witaj {{ username }}</p> {# Jeśli potrzebujesz HTML, użyj |safe tylko dla zaufanych danych #} {{ trusted_html|safe }}
W Flask/Jinja2:
JINJA21 2{# Jinja2 również automatycznie escapuje #} <p>Witaj {{ username|e }}</p>
W FastAPI z szablonami:
Python1 2 3 4 5 6 7 8 9from fastapi.templating import Jinja2Templates templates = Jinja2Templates(directory="templates") @app.get("/") def read_item(request: Request): return templates.TemplateResponse("index.html", { "request": request, "username": html.escape(username)# Ręczne escapowanie jeśli potrzeba })
Content Security Policy (CSP)
Włącz CSP (Content Security Policy) w nagłówkach HTTP, aby dodatkowo ograniczyć wykonywanie skryptów:
W Django:
Python1 2 3 4 5 6 7 8# settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', # ... ] # Dodaj do nagłówków odpowiedzi SECURE_CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'self'"
W Flask/FastAPI:
Python1 2 3 4@app.after_request def add_security_headers(response): response.headers["Content-Security-Policy"] = "default-src 'self'" return response
Nigdy nie renderuj niebezpiecznego HTML-a z wejścia użytkownika bez odpowiedniego escapowania lub sanitizacji.
CSRF – Cross-Site Request Forgery
CSRF to atak, który polega na wysłaniu żądania w imieniu zalogowanego użytkownika bez jego wiedzy. Przykład: użytkownik jest zalogowany do banku, wchodzi na złośliwą stronę, która wysyła POST /transfer, zmieniając hasło lub wykonując inną akcję w jego imieniu.
Ochrona w Django
Django ma wbudowaną ochronę CSRF:
Middleware (włączone domyślnie):
Python1 2 3 4 5# settings.py MIDDLEWARE = [ 'django.middleware.csrf.CsrfViewMiddleware', # ... ]
Tag w formularzu:
DJANGO1 2 3 4<form method="POST"> {% csrf_token %} <!-- reszta formularza --> </form>
Django automatycznie generuje token CSRF i weryfikuje go przy każdym żądaniu POST.
Dla AJAX:
JavaScript1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25// Pobierz token z cookies function getCookie(name) { let cookieValue = null; if (document.cookie && document.cookie !== '') { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } // Dodaj do nagłówków fetch('/api/endpoint/', { method: 'POST', headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json', }, body: JSON.stringify(data) });
Ochrona w FastAPI
FastAPI nie ma CSRF domyślnie, ale można użyć tokenów i nagłówków:
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22from fastapi import FastAPI, Request, Header, HTTPException import secrets SECRET_TOKEN = secrets.token_urlsafe(32) @app.post("/transfer") async def transfer(request: Request, csrf_token: str = Header(None)): # Pobierz token z sesji session_token = request.session.get('csrf_token') if not csrf_token or csrf_token != session_token: raise HTTPException(status_code=403, detail="CSRF token invalid") # Wykonaj akcję ... # Generuj token przy GET @app.get("/form") async def show_form(request: Request): token = secrets.token_urlsafe(32) request.session['csrf_token'] = token return {"csrf_token": token}
Bezpieczne przechowywanie haseł
Nigdy nie przechowuj haseł w postaci jawnej. W Pythonie używaj funkcji hashujących z solą (salt), które są zaprojektowane specjalnie do hashowania haseł — są one wolne, co utrudnia ataki brute-force.
Przykład z Django
Django używa PBKDF2 domyślnie, co jest bezpiecznym algorytmem:
Python1 2 3 4 5 6 7 8 9from django.contrib.auth.hashers import make_password, check_password # Hashowanie hasła hashed = make_password("mojehaslo") # Weryfikacja hasła is_valid = check_password("mojehaslo", hashed)# True # Django automatycznie używa PBKDF2 z losową solą
Django automatycznie używa PBKDF2 z losową solą, co zapewnia wysokie bezpieczeństwo.
W Flask/FastAPI
Użyj passlib lub bcrypt:
Bash1pip install passlib[bcrypt]
Python1 2 3 4 5 6 7from passlib.hash import bcrypt # Hashowanie hasła hashed = bcrypt.hash("mojehaslo") # Weryfikacja hasła is_valid = bcrypt.verify("mojehaslo", hashed)
Lub bezpośrednio bcrypt:
Python1 2 3 4 5 6 7 8import bcrypt # Hashowanie z automatyczną solą password = "mojehaslo".encode('utf-8') hashed = bcrypt.hashpw(password, bcrypt.gensalt()) # Weryfikacja is_valid = bcrypt.checkpw(password, hashed)
Nigdy nie używaj
- SHA1, MD5 – są szybkie i podatne na ataki rainbow tables
- SHA256 bez soli – można użyć rainbow tables
- Proste algorytmy hashujące – nie są zaprojektowane do haseł
Używaj tylko algorytmów zaprojektowanych specjalnie do hashowania haseł: bcrypt, argon2, PBKDF2, scrypt.
HTTPS i bezpieczne nagłówki
Każda aplikacja webowa powinna działać tylko po HTTPS. Używaj darmowych certyfikatów (np. Let's Encrypt) i wymuś bezpieczne nagłówki HTTP.
Konfiguracja w Django
Python1 2 3 4 5 6 7 8 9 10# settings.py (tylko w produkcji!) SECURE_SSL_REDIRECT = True# Przekieruj HTTP na HTTPS SECURE_HSTS_SECONDS = 31536000# HTTP Strict Transport Security (1 rok) SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True SECURE_CONTENT_TYPE_NOSNIFF = True# Zapobiega MIME sniffing SECURE_BROWSER_XSS_FILTER = True# Włącz filtr XSS w przeglądarce SESSION_COOKIE_SECURE = True# Cookies tylko przez HTTPS CSRF_COOKIE_SECURE = True# CSRF cookie tylko przez HTTPS X_FRAME_OPTIONS = 'DENY'# Zapobiega clickjacking
Konfiguracja w Nginx
NGINX1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21# Wymuś HTTPS server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; # Bezpieczne nagłówki add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; }
Bezpieczne nagłówki w Flask/FastAPI
Python1 2 3 4 5 6 7 8@app.after_request def add_security_headers(response): response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["X-XSS-Protection"] = "1; mode=block" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" return response
Inne dobre praktyki bezpieczeństwa
Oprócz wymienionych wyżej zagrożeń, warto pamiętać o następujących praktykach:
1. Ogranicz dane w logach
Nigdy nie zapisuj haseł, tokenów, numerów kart kredytowych ani innych wrażliwych danych w logach:
Python1 2 3 4 5# Źle logger.info(f"User {username} logged in with password {password}") # Dobrze logger.info(f"User {username} logged in")
2. Waliduj wszystkie dane wejściowe
Zawsze waliduj dane wejściowe użytkownika na poziomie serwera, nawet jeśli masz walidację po stronie klienta:
W Django:
Python1 2 3 4 5 6 7 8 9 10 11 12from django import forms class UserForm(forms.Form): username = forms.CharField(max_length=100, required=True) email = forms.EmailField(required=True) age = forms.IntegerField(min_value=0, max_value=120) def my_view(request): form = UserForm(request.POST) if form.is_valid(): # Tylko tutaj używaj danych username = form.cleaned_data['username']
W FastAPI:
Python1 2 3 4 5 6 7 8 9 10 11from pydantic import BaseModel, EmailStr, Field class UserCreate(BaseModel): username: str = Field(..., min_length=3, max_length=100) email: EmailStr age: int = Field(..., ge=0, le=120) @app.post("/users") def create_user(user: UserCreate): # Dane są automatycznie walidowane przez Pydantic ...
3. Używaj secrets zamiast random
Do generowania tokenów i sekretów używaj modułu secrets, nie random:
Python1 2 3 4 5 6 7 8import secrets # Dobrze - kryptograficznie bezpieczny token = secrets.token_urlsafe(32) # Źle - nie jest kryptograficznie bezpieczny import random token = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=32))
4. Aktualizuj zależności
Regularnie aktualizuj frameworki i biblioteki, szczególnie te związane z bezpieczeństwem:
Bash1 2 3 4 5 6# Sprawdź przestarzałe pakiety pip list --outdated # Użyj safety do wykrywania podatnych pakietów pip install safety safety check
5. Narzędzia bezpieczeństwa
bandit – statyczna analiza kodu Python w poszukiwaniu problemów bezpieczeństwa:
Bash1 2pip install bandit bandit -r ./
safety – wykrywanie podatnych pakietów w requirements.txt:
Bash1 2pip install safety safety check
django-secure – dodatkowe kontrole bezpieczeństwa dla Django:
Bash1pip install django-secure
6. Zabezpiecz admina Django
Django admin to potężne narzędzie, które wymaga szczególnej ochrony:
- Niestandardowy URL (np.
/control/zamiast/admin/):
Python1 2 3 4 5 6 7# urls.py from django.contrib import admin from django.urls import path urlpatterns = [ path('control/', admin.site.urls),# Zmień URL ]
- Dostęp tylko z określonych IP:
Python1 2 3 4 5 6# settings.py ALLOWED_HOSTS = ['yourdomain.com'] # W Nginx lub firewall # allow 192.168.1.0/24; # deny all;
- 2FA przez django-otp:
Bash1pip install django-otp
7. Rate limiting
Ogranicz liczbę żądań, aby zapobiec atakom brute-force i DoS:
W Django:
Bash1pip install django-ratelimit
Python1 2 3 4 5from django_ratelimit.decorators import ratelimit @ratelimit(key='ip', rate='5/m', method='POST') def login_view(request): ...
W FastAPI:
Python1 2 3 4 5 6 7 8 9 10from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) app.state.limiter = limiter @app.post("/login") @limiter.limit("5/minute") def login(request: Request): ...
Podsumowanie
Bezpieczeństwo w Python web apps to nie zbiór trików, a sposób myślenia. Każdy formularz, endpoint czy widok to potencjalny wektor ataku. Twoim zadaniem jako developera jest przewidzieć, co może pójść źle — i temu zapobiec.
Zadbaj o:
- bezpieczne zapytania (ORM zamiast surowego SQL),
- escaping danych (ochrona przed XSS),
- tokeny CSRF (ochrona przed CSRF),
- szyfrowane hasła (bcrypt, PBKDF2, argon2),
- HTTPS i poprawne nagłówki (HSTS, X-Frame-Options, CSP),
- walidację danych wejściowych (zawsze po stronie serwera),
- regularne aktualizacje (frameworki i biblioteki),
- monitoring i logowanie (bez wrażliwych danych).
Bo w bezpieczeństwie obowiązuje jedna zasada: "Nie pytaj, czy ktoś spróbuje — zapytaj, kiedy." Frameworki takie jak Django oferują wiele wbudowanych zabezpieczeń, ale to Ty jako programista musisz je właściwie skonfigurować i używać. Bezpieczeństwo to proces ciągły, a nie jednorazowa konfiguracja.



