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.

Sicurezza Django: OWASP Top 10 applicato al codice

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 = True

SECURE_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-audit

Scansiona 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 = 1

Dopo 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 = True

Le 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 True

Una 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.