Django è sicuro per default — ma non basta
Django ha una reputazione meritata per la sicurezza. Il sistema di template escapa l'output automaticamente, l'ORM parametrizza le query, il middleware CSRF protegge i form. Per molte vulnerabilità OWASP, Django fa il lavoro al posto tuo.
Ma "sicuro per default" non significa "sicuro a prescindere da come lo usi". Alcune vulnerabilità emergono da configurazioni errate, da scelte architetturali, o da codice che bypassa i meccanismi di protezione. Conoscere dove Django ti protegge e dove no è la base per costruire applicazioni sicure.

A01 — Broken Access Control
È la vulnerabilità più critica nella OWASP Top 10 2021 e la più comune nelle applicazioni Django che vedo in produzione.
Dove Django non ti protegge automaticamente
Django fornisce il sistema di autenticazione e i permessi a livello di modello, ma il controllo che un utente possa accedere a uno specifico oggetto è responsabilità tua. Se scrivi:
def get_progetto(request, progetto_id):
progetto = Progetto.objects.get(id=progetto_id)
return JsonResponse({"titolo": progetto.titolo})Qualsiasi utente autenticato può accedere a qualsiasi progetto semplicemente cambiando l'ID nell'URL. Il fix richiede un filtro esplicito:
def get_progetto(request, progetto_id):
progetto = get_object_or_404(
Progetto,
id=progetto_id,
responsabile=request.user
)
return JsonResponse({"titolo": progetto.titolo})Per permessi a livello di oggetto più complessi, django-guardian è lo strumento corretto — permette di assegnare permessi granulari per singola istanza del modello.
Il decorator @login_required non basta
@login_required verifica solo che l'utente sia autenticato, non che abbia i permessi per quella risorsa specifica. Usalo sempre, ma aggiungici sempre il controllo di autorizzazione.
A02 — Cryptographic Failures
Password
Django usa PBKDF2 con SHA256 per default — sicuro, ma puoi alzare il livello usando Argon2 (vincitore della Password Hashing Competition):
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
]Dati sensibili
Django non cifra i campi del database automaticamente. Se conservi dati sensibili — numeri di carta di credito, codici fiscali, dati sanitari — devi cifrare a livello applicativo. django-encrypted-model-fields è la soluzione più usata.
HTTPS
In produzione, configura questi settings:
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = TrueSECURE_SSL_REDIRECT fa il redirect automatico da HTTP a HTTPS. Gli altri impostano i cookie come secure-only e abilitano HSTS.
A03 — Injection
SQL Injection
Django ORM parametrizza automaticamente tutte le query — usando l'ORM correttamente, SQL injection è praticamente impossibile:
Progetto.objects.filter(titolo=user_input)Genera WHERE titolo = %s con user_input come parametro separato, mai concatenato nella query.
Il rischio emerge quando usi raw() o extra() con interpolazione di stringhe:
Progetto.objects.raw(f"SELECT * FROM progetti WHERE titolo = '{user_input}'")Questo è vulnerabile. La versione sicura usa i parametri:
Progetto.objects.raw(
"SELECT * FROM progetti WHERE titolo = %s",
[user_input]
)Template Injection
Il sistema di template Django escapa l'output per default. Il filtro {{ variabile }} converte automaticamente <, >, &, ", ' nelle entità HTML corrispondenti.
Il rischio è il filtro |safe e mark_safe() — dicono esplicitamente a Django di non escapare il contenuto. Usali solo con contenuto che controlli completamente, mai con input utente.
A04 — Insecure Design
Questa categoria riguarda decisioni architetturali, non bug specifici. Alcuni pattern da evitare in Django:
Esporre gli ID incrementali del database negli URL permette a un attaccante di enumerare le risorse. Usa UUID come primary key per le risorse pubbliche:
import uuid
from django.db import models
class Progetto(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)Non mettere logica di sicurezza nei template — un template che mostra o nasconde elementi basandosi sulle variabili di contesto è una protezione visiva, non reale. La verifica dei permessi va sempre nel backend.
A05 — Security Misconfiguration
DEBUG = True in produzione
Il problema più comune e più pericoloso. Con DEBUG = True, Django mostra stack trace completi con variabili locali, settings, e query SQL a chiunque causi un errore. Informazioni preziose per un attaccante.
Verifica sempre:
DEBUG = False
ALLOWED_HOSTS = ["tuodominio.com"]
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")SECRET_KEY hardcoded
La SECRET_KEY deve essere una variabile d'ambiente, mai nel codice sorgente. Viene usata per firmare sessioni, CSRF token, e altri meccanismi di sicurezza — se trapela, l'intera applicazione è compromessa.
Admin esposto sul path default
/admin/ è il path che tutti i bot cercano. Spostarlo riduce la superficie di attacco:
urlpatterns = [
path("gestione-interna/", admin.site.urls),
]A06 — Vulnerable and Outdated Components
Django rilascia patch di sicurezza regolarmente. Tieni sempre aggiornato Django e tutte le dipendenze.
Un modo pratico per monitorare le vulnerabilità nelle dipendenze è pip-audit:
pip install pip-audit
pip-auditScansiona il tuo requirements.txt contro il database delle vulnerabilità note e riporta quelle che richiedono aggiornamento.
Nella pipeline CI/CD di Skara, pip-audit gira ad ogni push — se trova vulnerabilità critiche, il deploy non parte.
A07 — Identification and Authentication Failures
Rate limiting sul login
Django non limita i tentativi di login per default. Un attaccante può provare migliaia di password senza essere bloccato. django-axes risolve questo:
INSTALLED_APPS = ["axes"]
AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 1Dopo 5 tentativi falliti, l'utente viene bloccato per un'ora.
Autenticazione a due fattori
Per applicazioni con dati sensibili, 2FA non è opzionale. django-otp è la libreria più matura — supporta TOTP (Google Authenticator, Authy) e backup codes. In Skara è abilitato per default per tutti gli utenti con accesso all'admin.
Session management
SESSION_COOKIE_AGE = 3600
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_COOKIE_HTTPONLY = TrueLe sessioni scadono dopo un'ora di inattività e quando si chiude il browser. HTTPONLY impedisce l'accesso ai cookie via JavaScript.
A08 — Software and Data Integrity Failures
Verifica le dipendenze
Usa pip-audit come menzionato sopra. Per le dipendenze JavaScript (se hai un frontend), npm audit fa lo stesso lavoro.
Firma i deploy
Nella pipeline di Skara, ogni immagine Docker viene firmata con il digest SHA256 prima del push su GHCR. Il deploy verifica il digest prima di avviare il container — garantisce che l'immagine in produzione sia esattamente quella che ha passato i test.
A09 — Security Logging and Monitoring Failures
Django loga gli errori ma non loga gli eventi di sicurezza per default. Configura il logging per catturare almeno:
LOGGING = {
"version": 1,
"handlers": {
"security": {
"class": "logging.FileHandler",
"filename": "/var/log/django/security.log",
}
},
"loggers": {
"django.security": {
"handlers": ["security"],
"level": "WARNING",
"propagate": False,
}
},
}django.security cattura gli eventi di sicurezza built-in — tentivi CSRF falliti, accessi negati, e altri warning di sicurezza del framework.
Per un monitoring più completo, Sentry cattura eccezioni in produzione con contesto completo — utile sia per il debugging che per identificare tentativi di attacco.
A10 — Server-Side Request Forgery (SSRF)
SSRF si manifesta quando un'applicazione fa richieste HTTP a URL forniti dall'utente — feed RSS, webhook, preview di link. Un attaccante può far puntare l'URL a risorse interne della rete (metadata AWS, servizi interni, database).
Se la tua applicazione Django fa richieste HTTP a URL controllati dall'utente, valida sempre l'URL prima di eseguire la richiesta:
from urllib.parse import urlparse
def is_safe_url(url: str) -> bool:
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
return False
if parsed.hostname in ("localhost", "127.0.0.1", "0.0.0.0"):
return False
return TrueUna validazione minimale — per contesti ad alto rischio è necessario un controllo più robusto sulle reti private (10.x.x.x, 192.168.x.x, 172.16.x.x).
La security checklist prima del deploy
Prima di ogni rilascio in produzione, Skara passa attraverso questa checklist:
- DEBUG = False e ALLOWED_HOSTS configurato
- SECRET_KEY da variabile d'ambiente
- pip-audit senza vulnerabilità critiche
- HTTPS forzato con HSTS
- Cookie secure e httponly
- Rate limiting sul login attivo
- 2FA abilitato per gli utenti admin
- Logging di sicurezza configurato
Non è una lista esaustiva — è il minimo che non può mancare.