Bezpieczeństwo w Python web apps

Kacper Sieradziński
Kacper Sieradziński
24 kwietnia 2025Edukacja5 min czytania

Bezpieczeństwo aplikacji webowych to nie dodatek – to obowiązek. Każdy system narażony na dostęp z sieci musi być projektowany z myślą o tym, że ktoś kiedyś spróbuje go złamać. Frameworki takie jak Django, Flask i FastAPI oferują wiele mechanizmów ochrony, ale żaden framework nie zastąpi zrozumienia zagrożeń i świadomości programisty. W tym przewodniku poznasz najczęstsze zagrożenia bezpieczeństwa w aplikacjach webowych oraz jak się przed nimi chronić, korzystając z najlepszych praktyk i narzędzi dostępnych w ekosystemie Pythona.

Obraz główny Bezpieczeństwo w Python web apps

Najczęstsze zagrożenia w aplikacjach webowych

Według organizacji OWASP (Open Web Application Security Project), najczęstsze ataki to:

  1. SQL Injection – wstrzyknięcie kodu SQL poprzez dane wejściowe użytkownika.
  2. XSS (Cross-Site Scripting) – wstrzyknięcie złośliwego kodu JavaScript do aplikacji.
  3. CSRF (Cross-Site Request Forgery) – nieautoryzowane żądania wykonywane w imieniu zalogowanego użytkownika.
  4. Brute-force – łamanie haseł metodą prób i błędów.
  5. Insecure Deserialization – odczytanie niebezpiecznych danych z sesji.
  6. 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

Python
1 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:

SQL
1 admin' OR 1=1; --

To spowoduje, że zapytanie stanie się:

SQL
1 SELECT * 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:

Python
1 2 3 4 5 6 from 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):

Python
1 2 3 from 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:

Python
1 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

Python
1 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:

JavaScript
1 ?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:

DJANGO
1 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:

JINJA2
1 2 {# Jinja2 również automatycznie escapuje #} <p>Witaj {{ username|e }}</p>

W FastAPI z szablonami:

Python
1 2 3 4 5 6 7 8 9 from 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:

Python
1 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:

Python
1 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):

Python
1 2 3 4 5 # settings.py MIDDLEWARE = [ 'django.middleware.csrf.CsrfViewMiddleware', # ... ]

Tag w formularzu:

DJANGO
1 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:

JavaScript
1 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:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from 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:

Python
1 2 3 4 5 6 7 8 9 from 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:

Bash
1 pip install passlib[bcrypt]
Python
1 2 3 4 5 6 7 from passlib.hash import bcrypt # Hashowanie hasła hashed = bcrypt.hash("mojehaslo") # Weryfikacja hasła is_valid = bcrypt.verify("mojehaslo", hashed)

Lub bezpośrednio bcrypt:

Python
1 2 3 4 5 6 7 8 import 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

Python
1 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

NGINX
1 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

Python
1 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:

Python
1 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:

Python
1 2 3 4 5 6 7 8 9 10 11 12 from 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:

Python
1 2 3 4 5 6 7 8 9 10 11 from 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:

Python
1 2 3 4 5 6 7 8 import 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:

Bash
1 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:

Bash
1 2 pip install bandit bandit -r ./

safety – wykrywanie podatnych pakietów w requirements.txt:

Bash
1 2 pip install safety safety check

django-secure – dodatkowe kontrole bezpieczeństwa dla Django:

Bash
1 pip 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/):
Python
1 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:
Python
1 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:
Bash
1 pip install django-otp

7. Rate limiting

Ogranicz liczbę żądań, aby zapobiec atakom brute-force i DoS:

W Django:

Bash
1 pip install django-ratelimit
Python
1 2 3 4 5 from django_ratelimit.decorators import ratelimit @ratelimit(key='ip', rate='5/m', method='POST') def login_view(request): ...

W FastAPI:

Python
1 2 3 4 5 6 7 8 9 10 from 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.