L'implementazione della politica di sicurezza dei contenuti (CSP, Content Security Policy) è una delle pratiche fondamentali per migliorare la sicurezza delle applicazioni web moderne. La CSP aiuta a prevenire vari tipi di attacchi, come l'iniezione di script (XSS) e il furto di dati sensibili, imponendo restrizioni su ciò che può essere caricato e eseguito nel browser. L'uso di una politica rigorosa di CSP è particolarmente importante per le applicazioni web che gestiscono dati sensibili o sono esposte a un pubblico generico.

Un approccio comune per implementare la CSP in un'applicazione basata su FastAPI è l'uso di un middleware che applica le regole della CSP a tutte le risposte HTTP. Di seguito viene esemplificato come configurare correttamente una politica di sicurezza dei contenuti per un'applicazione FastAPI utilizzando il middleware BaseHTTPMiddleware.

python
CSP_POLICY = ( "default-src 'none'; " "script-src 'self'; " "style-src 'self'; " "img-src 'self' data:; " "font-src 'self'; " "connect-src 'self' https://api.trusted.com; " "object-src 'none'; " "frame-ancestors 'none';" ) class ContentSecurityPolicyMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response: Response =
await call_next(request) response.headers['Content-Security-Policy'] = CSP_POLICY return response app = FastAPI() app.add_middleware(ContentSecurityPolicyMiddleware)

Con questo middleware registrato, ogni risposta HTTP in uscita avrà l'intestazione CSP, che informa il browser sulle restrizioni da applicare. Il browser applicherà immediatamente queste regole ogni volta che la pagina viene caricata, rifiutando il caricamento o l'esecuzione di qualsiasi contenuto che violi la politica definita. La politica di sicurezza dei contenuti di solito viene applicata uniformemente per le API e i pannelli di amministrazione, ma può essere configurata in modo condizionale per distinguere tra rotte API e HTML.

Blocco degli script e degli stili inline

Una delle caratteristiche più visibili della CSP è la sua capacità di bloccare script inline o stili CSS che non rispettano la politica. Ad esempio, la politica predefinita di questa configurazione (script-src 'self') proibisce l'esecuzione di script inline, compreso il codice JavaScript scritto direttamente in HTML o tramite attributi di eventi come onclick.

Se un'applicazione include uno script inline come:

html
<script>alert('Hello, world!');</script>

Il browser non eseguirà questo script e mostrerà un errore nella console degli sviluppatori, indicando che la violazione della CSP ha impedito l'esecuzione del codice. Questo comportamento è utile per prevenire vulnerabilità XSS (Cross-Site Scripting), dove un attaccante potrebbe iniettare codice dannoso direttamente nelle pagine web.

Consenti script inline solo quando necessario

Se per qualche motivo è necessario permettere l'uso di script inline, si può aggiungere 'unsafe-inline' alla direttiva script-src della CSP. Tuttavia, questa pratica è fortemente sconsigliata, a meno che non si stia trattando con codice legacy o casi inevitabili, in quanto riapre i rischi legati agli attacchi XSS.

python
CSP_POLICY = ( "default-src 'none'; " "script-src 'self' 'unsafe-inline'; " # ... altre regole ... )

Questa modifica consente l'esecuzione di script inline, ma va trattata con cautela. Utilizzare 'unsafe-inline' annulla uno dei principali vantaggi della CSP, che è proprio la protezione contro l'inserimento di codice maligno direttamente nel browser.

Blocco degli stili inline

Similmente agli script, anche gli stili CSS inline possono rappresentare un rischio per la sicurezza se non correttamente gestiti. La politica di sicurezza dei contenuti di base, che consente solo stili provenienti da fonti esterne (come i file CSS), blocca l'uso di stili definiti direttamente nei tag HTML.

Se si desidera permettere l'uso di stili inline, è possibile aggiungere 'unsafe-inline' anche alla direttiva style-src, ma anche in questo caso si tratta di una modifica che potrebbe indebolire la sicurezza dell'applicazione.

Politiche dinamiche per API e pagine HTML

Se l'applicazione serve sia rotte API che rotte HTML, è possibile configurare la CSP in modo dinamico a seconda del tipo di contenuto richiesto. Ad esempio, le rotte API potrebbero non richiedere restrizioni così severe come le rotte che restituiscono pagine HTML, dove una protezione più rigorosa contro gli script inline è fondamentale.

In questi casi, il middleware di CSP potrebbe includere logiche condizionali per impostare diverse politiche in base al percorso della richiesta, garantendo che le API non siano eccessivamente limitate, pur mantenendo un alto livello di protezione per le pagine web.

Considerazioni finali

Implementare una politica di sicurezza dei contenuti adeguata è un passo fondamentale nella protezione delle applicazioni web moderne. La configurazione corretta della CSP non solo previene l'esecuzione di script dannosi, ma contribuisce anche a ridurre il rischio di attacchi come il clickjacking e il furto di dati sensibili.

Un aspetto cruciale della CSP è che le politiche devono essere bilanciate in modo tale da non ostacolare funzionalità legittime, ma senza compromettere la sicurezza. Per esempio, se l'applicazione ha bisogno di integrare contenuti da fonti esterne, è necessario configurare le direttive in modo da permettere solo quelle fonti specifiche che sono fidate, senza aprire la porta a potenziali minacce.

Come Scrivere Test con Pytest: Strategie per una Verifica Efficace del Codice

Quando si sviluppa un'applicazione, è essenziale garantirne la qualità attraverso test accurati e completi. In Python, uno degli strumenti più potenti e diffusi per la scrittura di test è Pytest. Questo framework consente di testare facilmente funzioni, moduli e interi sistemi, con una sintassi chiara e funzionalità avanzate che facilitano il lavoro di sviluppo. In questa sezione esploreremo le modalità principali per utilizzare Pytest in modo efficiente, concentrandoci su funzioni pure, fixture per l'inizializzazione e la pulizia, mock per le dipendenze esterne e test di integrazione.

Iniziamo con i test di funzioni "pure", cioè quelle che non hanno effetti collaterali né dipendenze esterne. Pytest riconosce automaticamente i file che iniziano con test_ o terminano con _test.py, eseguendo ogni funzione che ha il prefisso test_. In questi test, l'assertion è il principale strumento per verificare i risultati. Per esempio, supponiamo di avere una funzione add_numbers che somma due numeri e una funzione is_even che verifica se un numero è pari. Il test per queste funzioni potrebbe essere il seguente:

python
from utils.math_utils import add_numbers, is_even
def test_add_numbers(): assert add_numbers(2, 3) == 5 assert add_numbers(-1, 1) == 0 assert add_numbers(0, 0) == 0 def test_is_even(): assert is_even(4) is True assert is_even(7) is False

Quando eseguiamo Pytest nella radice del progetto, verranno automaticamente scoperti ed eseguiti tutti i test, con un output chiaro che riporta il risultato di ciascun test.

Utilizzo delle Fixture per l'Inizializzazione e la Pulizia

Nel mondo reale, le applicazioni raramente operano in isolamento. Molte funzioni o classi richiedono uno stato noto, come un oggetto inizializzato, una connessione a un database o un file temporaneo. Pytest fornisce un sistema di fixture che permette di definire logiche di configurazione riutilizzabili e parametriche, controllabili a livello di funzione, modulo o sessione. Ad esempio, supponiamo di dover testare una funzione che interagisce con un file temporaneo. Possiamo definire una fixture che crea e distrugge automaticamente il file per ogni test:

python
import pytest import tempfile import os @pytest.fixture def temp_file(): # Setup: crea un file temporaneo fd, path = tempfile.mkstemp() os.close(fd) yield path # Teardown: rimuovi il file dopo il test if os.path.exists(path): os.remove(path) def test_write_and_read(temp_file): with open(temp_file, "w") as f: f.write("hello world") with open(temp_file, "r") as f: data = f.read() assert data == "hello world"

In questo esempio, la fixture temp_file crea un file temporaneo prima del test e lo elimina alla fine, anche se il test fallisce. Pytest garantisce che la fixture venga eseguita prima del test e che la pulizia avvenga dopo, indipendentemente dal risultato del test.

Mocking delle Dipendenze Esterne

Un buon test unitario non deve mai dipendere da risorse esterne come API, database o servizi di rete. Pytest consente di sostituire queste dipendenze con "mock" controllati, rendendo i test più rapidi e deterministici. In combinazione con il modulo unittest.mock di Python, è possibile simulare qualsiasi oggetto o funzione.

Supponiamo di voler testare una funzione che invia una email, ma senza inviarla realmente. Per fare ciò, possiamo usare patch per sostituire la funzione send_email con un mock:

python
from utils.email_utils import notify_user
from unittest.mock import patch def test_notify_user(): user = type("User", (), {"email": "[email protected]"})() with patch("utils.email_utils.send_email") as mock_send: notify_user(user, "Hello!") mock_send.assert_called_once_with("[email protected]", "Notification", "Hello!")

In questo test, sostituire la funzione send_email ci consente di verificare che venga chiamata correttamente, senza inviare effettivamente una email. È possibile mockare qualsiasi oggetto o metodo, dai database ai client HTTP, per garantire che il codice venga testato in isolamento.

Test di Integrazione: Verifica dell'Integrazione tra Componenti

Mentre i test unitari verificano che i singoli componenti funzionino correttamente, i test di integrazione si concentrano sull'interazione tra più componenti. Questi test sono essenziali per verificare che i diversi pezzi del sistema, come database, cache e API, lavorino insieme come previsto. I test di integrazione sono più lenti dei test unitari, ma offrono una visione più completa della funzionalità dell'applicazione.

Un modo per eseguire test di integrazione è utilizzare Docker e Docker Compose per isolare i servizi reali come Redis o PostgreSQL. Configurando un file docker-compose.test.yml, possiamo facilmente creare un ambiente di test che simula la produzione. Ad esempio, un file per testare Redis potrebbe apparire così:

yaml
version: "3.9"
services: redis: image: redis:7 ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 2s timeout: 2s retries: 5

Questo approccio consente di eseguire test di integrazione contro un'istanza reale di Redis, che si avvia e si arresta automaticamente durante i test. Al termine dei test, possiamo distruggere l'ambiente con il comando:

bash
docker-compose -f docker-compose.test.yml down -v

Test di API con Client e Docker

Quando si eseguono test di integrazione su un'applicazione web, è essenziale simulare le richieste HTTP per verificare che le API funzionino correttamente. Con FastAPI, possiamo usare il TestClient per inviare richieste HTTP reali al server durante il test. Eseguire test contro un'applicazione in esecuzione in Docker è un approccio molto simile.

Ecco un esempio di test di un endpoint API che interagisce con Redis:

python
from fastapi import FastAPI, HTTPException import redis app = FastAPI() r = redis.Redis(host="localhost", port=6379, decode_responses=True) @app.post("/cache/set")
def set_value(key: str, value: str):
r.
set(key, value) return {"msg": "set"} @app.get("/cache/get") def get_value(key: str): value = r.get(key) if value is None: raise HTTPException(status_code=404, detail="Not found") return {"value": value}

Il test per questo endpoint può essere scritto come segue:

python
import pytest from fastapi.testclient import TestClient from app.main import app client = TestClient(app) @pytest.fixture(scope="module", autouse=True) def setup_redis(): import redis r = redis.Redis(host="localhost", port=6379) r.flushdb() yield r.flushdb() def test_set_and_get_value():
response = client.post("/cache/set", params={"key": "mykey", "value": "myvalue"})
assert response.status_code == 200 response = client.get("/cache/get?key=mykey") assert response.status_code == 200 assert response.json() == {"value": "myvalue"}

Considerazioni Finali

Quando si scrivono test, è fondamentale adottare un approccio metodico e strutturato, tenendo conto delle interazioni tra le diverse parti del sistema. Pytest, con le sue capacità di fixture, mocking e test di integrazione, offre una solida base per garantire che ogni componente funzioni correttamente da solo e all'interno dell'intero sistema. Le best practices suggeriscono di scrivere test per ogni regola di business critica, ogni verifica di sicurezza e ogni branch complesso del codice. In questo modo, è possibile mantenere alta la qualità del codice, minimizzando i rischi di regressioni e malfunzionamenti in produzione.

Come Creare e Gestire un Ambiente Virtuale in Python per un Progetto di Sviluppo

Iniziamo con un passaggio fondamentale per chi lavora con Python, particolarmente su sistemi basati su Linux: l’installazione della versione più recente di Python, in questo caso la 3.11, che offre funzionalità avanzate e miglioramenti rispetto alle versioni precedenti. Sebbene Python venga spesso preinstallato, è importante garantirsi di avere sempre l'accesso alle versioni più recenti per sfruttare al meglio tutte le caratteristiche del linguaggio. Il primo passo da compiere è preparare il gestore di pacchetti e aggiungere un repository affidabile:

bash
sudo apt update sudo apt install -y software-properties-common sudo add-apt-repository ppa:deadsnakes/ppa sudo apt update sudo apt install -y python3.11 python3.11-venv python3.11-dev build-essential

Questi comandi eseguono una serie di operazioni in un colpo solo. Prima di tutto, aggiornano il sistema con l'ultima lista dei pacchetti disponibili, per evitare problemi durante l'installazione. Successivamente, vengono installati gli strumenti necessari per aggiungere nuovi repository. Il PPA deadsnakes è una fonte affidabile per ottenere le versioni più recenti di Python su Ubuntu. Dopo aver aggiornato nuovamente la lista dei pacchetti, viene installata la versione 3.11 di Python, il modulo per ambienti virtuali, gli header di sviluppo e gli strumenti necessari per la compilazione di estensioni in C. A questo punto, siamo pronti per creare un ambiente Python isolato per il nostro progetto.

Creazione di un Ambiente Virtuale

Una delle pratiche più importanti nello sviluppo con Python è l'isolamento dell’ambiente di lavoro. Creare un ambiente virtuale è essenziale per mantenere al sicuro l'installazione globale di Python, evitare conflitti tra dipendenze e permettere ad ogni progetto di gestire le proprie necessità specifiche. Il seguente comando crea un ambiente virtuale nella directory del nostro progetto:

bash
python3.11 -m venv .env

Questo comando genera una directory chiamata .env, che contiene un interprete Python fresco e uno spazio dedicato per le dipendenze. Una volta creato l’ambiente, è fondamentale attivarlo per lavorare nel contesto isolato:

bash
source .env/bin/activate

Da questo momento in poi, ogni comando o installazione riguarderà solo questo ambiente, proteggendo il Python di sistema e gli altri progetti. Se mai dovessimo voler tornare alla shell di sistema, è sufficiente eseguire il comando deactivate.

Automazione dei Compiti di Routine

Man mano che il nostro progetto cresce, diventa sempre più necessario automatizzare le operazioni ripetitive. Dall’installazione delle dipendenze all’esecuzione dei test, dalla manutenzione della qualità del codice all'avvio del server di sviluppo, queste attività possono essere gestite con maggiore efficienza attraverso l'automazione. Un file Makefile è uno strumento potente per semplificare e uniformare il nostro flusso di lavoro. Ecco un esempio di Makefile che può essere utilizzato per mantenere tutto in ordine:

makefile
install:
pip install -r requirements.txt lint: black --check . flake8 . test: pytest dev: uvicorn app.main:app --reload

Il comando install legge dal file requirements.txt e assicura che tutte le librerie principali—come FastAPI, Pydantic, SQLAlchemy, Alembic, Celery, Redis, Jinja2, e HTTPX—siano installate. Con lint, verifichiamo lo stile del codice con Black e Flake8, mantenendo il codice pulito e leggibile. Il target test lancia i test automatici tramite Pytest, mentre dev avvia il server di sviluppo FastAPI in modalità di ricarica automatica.

Gestione della Configurazione e dei Segreti

Quando un progetto cresce, la gestione delle impostazioni e dei segreti diventa una delle sfide principali. Organizzare i file di configurazione, come .env.example, alembic.ini e celeryconfig.py, in una directory dedicata, come config/, consente di mantenere chiarezza e sicurezza. Ogni variabile di configurazione troverà il suo posto qui, evitando il rischio che le impostazioni diventino disordinate o difficili da localizzare. Centralizzare la gestione dei credenziali, delle chiavi segrete e delle impostazioni di migrazione è una buona pratica che aumenta la prevedibilità e la manutenibilità del progetto.

Implementazione degli Endpoint CRUD

Un’applicazione robusta inizia con una solida gestione dei dati. La capacità di creare, leggere, aggiornare ed eliminare informazioni è il cuore di molte applicazioni moderne, come piattaforme di contenuti o siti di e-commerce. Prima di aggiungere funzionalità avanzate, la priorità deve essere quella di implementare metodi affidabili per consentire agli utenti o ai sistemi di interagire con i dati in modo strutturato, prevedibile e sicuro. In questa sezione, vedremo come creare il nostro primo set di funzionalità utilizzando FastAPI e Pydantic.

Supponiamo che stiamo sviluppando un sistema per gestire una collezione di libri. Ogni libro avrà un identificativo, un titolo, un autore, una descrizione opzionale e un anno di pubblicazione. Iniziamo con un semplice modello di dati:

python
from pydantic import BaseModel, Field
from typing import Optional class Book(BaseModel): id: int title: str = Field(..., min_length=1, max_length=255) author: str = Field(..., min_length=1, max_length=100) description: Optional[str] = Field(None, max_length=1000) year: int = Field(..., ge=1000, le=2100)

In questo esempio, utilizziamo la funzione Field di Pydantic per impostare limiti sulla lunghezza delle stringhe e sulle gamme numeriche. I campi obbligatori vengono definiti con ..., mentre quelli opzionali, come la descrizione, possono essere omessi o impostati a None. Se qualcuno tenta di inviare un libro senza titolo o con un anno fuori dal range, FastAPI e Pydantic restituiranno un errore di validazione chiaro e utile.

Progettazione del Livello di Servizio

Separare il codice che gestisce le richieste web dalla logica che interagisce con i dati è una pratica fondamentale per ottenere un’applicazione pulita e testabile. Questo approccio, noto come “service layer pattern”, ha due vantaggi principali: semplifica i test e migliora la manutenibilità. Il nostro livello di servizio gestirà la raccolta dei libri e risponderà alle richieste di aggiungere, aggiornare, recuperare o eliminare libri. Di seguito un esempio di un servizio in memoria che utilizza un dizionario Python:

python
from app.models import Book
from typing import Dict, List class BookService: def __init__(self): self._books: Dict[int, Book] = {} self._id_counter: int = 1 def create_book(self, data: Book) -> Book: data.id = self._id_counter self._books[self._id_counter] = data self._id_counter += 1 return data def get_book(self, book_id: int) -> Book: if book_id not in self._books: raise KeyError("Book not found") return self._books[book_id] def list_books(self) -> List[Book]: return list(self._books.values())
def update_book(self, book_id: int, data: Book) -> Book:
if book_id not in self._books: raise KeyError("Book not found") data.id = book_id self._books[book_id] = data return data def delete_book(self, book_id: int) -> None: if book_id not in self._books: raise KeyError("Book not found") del self._books[book_id]

In questo esempio, la classe BookService mantiene un dizionario di libri, indicizzati tramite il loro ID. Ogni libro nuovo ottiene un ID unico, e vengono esposti metodi per ogni operazione CRUD. Se un libro richiesto non esiste, viene sollevato un KeyError, che verrà gestito correttamente nella nostra API.

Creazione degli Endpoint FastAPI

Con il nostro modello di dati e il livello di servizio pronti, possiamo ora esporre le funzionalità CRUD come endpoint RESTful grazie a FastAPI. Ogni endpoint corrisponderà a un metodo HTTP comune:

  • POST per creare nuovi libri

  • GET per leggere informazioni sui libri

  • PUT/PATCH per aggiornare i libri esistenti

  • DELETE per eliminare i libri

Endtext