Database e web: Python, SQLite3 e sicurezza

// obiettivi di apprendimento
Capire il flusso request→Python→DBMS→response in un’applicazione web con backend Python
Usare il modulo sqlite3 di Python per connettersi, leggere e scrivere dati in un database
Comprendere cos’è la SQL Injection, come funziona un attacco e come prevenirlo con i prepared statement
Costruire con Flask una mini-applicazione web con lettura/scrittura su DB e visualizzazione dinamica
🎬
Video
Flask + SQLite3 + SQL Injection spiegati dal vivo
Guarda →
📄
Slides
Slide sqlite3, Flask, SQL injection e parametri
Scarica →
🧪
Lab
App Flask completa con form, DB e correzione SQLi
GitHub →
🔗
Risorse
Docs Python sqlite3, Flask quickstart, OWASP Top 10
Vedi →

Il backend: Python parla con il database

Finora abbiamo imparato a scrivere SQL in un client (MySQL Workbench, DBeaver). Nelle applicazioni reali, però, le query non le scrive un essere umano: le genera il codice. Il flusso standard è:

🌐
Browser
HTTP request
🐍
Python / Flask
logica applicativa
🗄️
SQLite3 / DBMS
esegue la query
📋
Risultati
righe → HTML
🌐
Browser
HTTP response

SQLite3 è un database embedded: non ha un server separato. Il database è un singolo file (.db) che vive nella stessa directory del progetto. È incluso nella libreria standard di Python: niente da installare. Perfetto per sviluppo, applicazioni desktop, progetti scolastici e prototipazione.

Il modulo sqlite3 di Python

Connessione, cursor e ciclo di vita

# ── 1. Connessione al file DB (viene creato se non esiste) ──────────────
import sqlite3

conn = sqlite3.connect("scuola.db")   # apre/crea il file scuola.db

# ── 2. Il cursor: "penna" per eseguire comandi SQL ──────────────────────
cursor = conn.cursor()

# ── 3. Creazione tabella (se non esiste) ────────────────────────────────
cursor.execute("""
    CREATE TABLE IF NOT EXISTS studenti (
        id      INTEGER PRIMARY KEY AUTOINCREMENT,
        nome    TEXT    NOT NULL,
        cognome TEXT    NOT NULL,
        anno    INTEGER NOT NULL
    )
""")

# ── 4. Salva le modifiche DDL/DML ───────────────────────────────────────
conn.commit()

# ── 5. Chiudi la connessione quando hai finito ──────────────────────────
conn.close()
MetodoCosa fa
connect("file.db")Apre/crea il database. Con ":memory:" usa la RAM (DB temporaneo)
cursor()Crea un oggetto Cursor per eseguire comandi SQL
execute(sql)Esegue una singola istruzione SQL
executemany(sql, lista)Esegue la stessa istruzione per ogni elemento della lista
fetchall()Restituisce tutte le righe del risultato come lista di tuple
fetchone()Restituisce la prima riga, poi la successiva ad ogni chiamata
commit()Salva definitivamente le modifiche (DML)
rollback()Annulla le modifiche non ancora committate
close()Chiude la connessione e libera le risorse

Inserimento e lettura dati

import sqlite3

# ── Inserimento di più studenti ──────────────────────────────────────────
with sqlite3.connect("scuola.db") as conn:
    cursor = conn.cursor()

    studenti = [
        ("Mario",   "Rossi",   4),
        ("Giulia",  "Bianchi", 3),
        ("Luca",    "Verdi",   4),
    ]

    cursor.executemany(
        "INSERT INTO studenti (nome, cognome, anno) VALUES (?, ?, ?)",
        studenti
    )
    conn.commit()
    print(f"Inseriti {cursor.rowcount} studenti")

# ── Lettura con filtro ───────────────────────────────────────────────────
with sqlite3.connect("scuola.db") as conn:
    conn.row_factory = sqlite3.Row      # righe accessibili come dict
    cursor = conn.cursor()

    anno_cercato = 4
    cursor.execute(
        "SELECT * FROM studenti WHERE anno = ?",
        (anno_cercato,)                 # ← tupla con i parametri
    )

    for row in cursor.fetchall():
        print(f"{row['nome']} {row['cognome']} — Anno {row['anno']}")
// context manager with

Usare with sqlite3.connect(...) as conn: è il modo consigliato: in caso di eccezione fa automaticamente il rollback; se tutto va bene chiama il commit. Evita di dimenticarsi di close().

SQL Injection: l’attacco più diffuso nei DB web

// pericolo — sql injection

La SQL Injection è nella OWASP Top 10 da quasi vent’anni. Si verifica quando un’applicazione concatena direttamente l’input dell’utente nella query SQL senza validazione. L’attaccante modifica la logica della query inserendo SQL arbitrario.

Esempio di codice vulnerabile

# ⛔ CODICE VULNERABILE — non fare mai così!

username = input("Username: ")     # l'utente digita: admin' --
password = input("Password: ")

# Costruisce la query con concatenazione di stringhe
query = "SELECT * FROM utenti WHERE username='" + username + \
        "' AND password='" + password + "'"

# Query risultante se username = "admin' --":
# SELECT * FROM utenti WHERE username='admin' --' AND password='...'
# Il -- commenta tutto il resto → password ignorata → accesso come admin!

cursor.execute(query)   # PERICOLOSISSIMO

Anatomia dell’attacco

Input utenteQuery generataEffetto
admin' --WHERE username='admin' --'...Login senza password
' OR '1'='1WHERE username='' OR '1'='1'Restituisce tutti gli utenti
'; DROP TABLE utenti; --...; DROP TABLE utenti; --Elimina l’intera tabella
' UNION SELECT username, password FROM utenti --UNION con tabella credenzialiData leak completo

La soluzione: Prepared Statement (query parametrizzate)

# ✅ CODICE SICURO — usa sempre i parametri (?)

username = input("Username: ")
password = input("Password: ")

# I ? sono segnaposto: sqlite3 li sostituisce in modo sicuro
cursor.execute(
    "SELECT * FROM utenti WHERE username = ? AND password = ?",
    (username, password)   # ← parametri come tupla SEPARATA dalla query
)

# Anche se username = "admin' --", sqlite3 lo tratta come stringa letterale:
# cerca username uguale a "admin' --", non trova nulla → login fallisce ✓

utente = cursor.fetchone()
if utente:
    print("Accesso consentito")
else:
    print("Credenziali errate")
// perché funziona

Con i prepared statement, la query viene compilata prima di inserire i parametri. Il DBMS sa già quale parte è “codice SQL” e quale è “dato”. Anche se l’utente inserisce del SQL, viene trattato come dato puro — nessun rischio di esecuzione. Regola d’oro: mai concatenare stringhe per costruire query SQL.

Flask: un’applicazione web minimale con SQLite3

Flask è un micro-framework Python per creare applicazioni web. Con poche righe di codice puoi avere route, template HTML e accesso al database.

// installazione
pip install flask

Struttura del progetto

progetto/
├── app.py ← logica Flask + query SQL
├── scuola.db ← database SQLite3
└── templates/
    ├── studenti.html ← template Jinja2 (lista)
    └── aggiungi.html ← template Jinja2 (form)

app.py — Backend completo

from flask import Flask, render_template, request, redirect, url_for
import sqlite3

app = Flask(__name__)
DB = "scuola.db"

# ── Helper: connessione con row_factory ─────────────────────────────────
def get_db():
    conn = sqlite3.connect(DB)
    conn.row_factory = sqlite3.Row   # righe come dizionari
    return conn

# ── Init DB all'avvio ────────────────────────────────────────────────────
def init_db():
    with get_db() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS studenti (
                id      INTEGER PRIMARY KEY AUTOINCREMENT,
                nome    TEXT NOT NULL,
                cognome TEXT NOT NULL,
                anno    INTEGER NOT NULL
            )
        """)

# ── Route GET: mostra tutti gli studenti ────────────────────────────────
@app.route("/")
def lista_studenti():
    with get_db() as conn:
        studenti = conn.execute(
            "SELECT * FROM studenti ORDER BY cognome"
        ).fetchall()
    return render_template("studenti.html", studenti=studenti)

# ── Route POST: aggiunge uno studente dal form ───────────────────────────
@app.route("/aggiungi", methods=["GET", "POST"])
def aggiungi():
    if request.method == "POST":
        nome    = request.form["nome"]
        cognome = request.form["cognome"]
        anno    = request.form["anno"]

        # ✅ Prepared statement — NESSUNA concatenazione!
        with get_db() as conn:
            conn.execute(
                "INSERT INTO studenti (nome, cognome, anno) VALUES (?, ?, ?)",
                (nome, cognome, anno)
            )
        return redirect(url_for("lista_studenti"))

    return render_template("aggiungi.html")

if __name__ == "__main__":
    init_db()
    app.run(debug=True)

templates/studenti.html — Visualizzazione dinamica con Jinja2

<!-- templates/studenti.html -->
<!DOCTYPE html>
<html><body>

<h1>Elenco Studenti</h1>
<a href="{{ url_for('aggiungi') }}">+ Aggiungi studente</a>

<table>
  <tr><th>ID</th><th>Nome</th><th>Cognome</th><th>Anno</th></tr>

  {% for s in studenti %}
  <tr>
    <td>{{ s['id'] }}</td>
    <td>{{ s['nome'] }}</td>
    <td>{{ s['cognome'] }}</td>
    <td>{{ s['anno'] }}ª</td>
  </tr>
  {% endfor %}
</table>

</body></html>

templates/aggiungi.html — Form di inserimento

<!-- templates/aggiungi.html -->
<h1>Aggiungi Studente</h1>

<!-- action punta alla route /aggiungi, metodo POST -->
<form action="{{ url_for('aggiungi') }}" method="POST">

  <label>Nome:
    <input type="text" name="nome" required>
  </label>

  <label>Cognome:
    <input type="text" name="cognome" required>
  </label>

  <label>Anno (1-5):
    <input type="number" name="anno" min="1" max="5" required>
  </label>

  <button type="submit">Salva</button>
</form>

Modificare dati: UPDATE via form pre-popolato

# ── Route GET/POST per modificare uno studente esistente ─────────────────
@app.route("/modifica/<int:id_studente>", methods=["GET", "POST"])
def modifica(id_studente):
    with get_db() as conn:
        if request.method == "POST":
            # Aggiorna il record — prepared statement obbligatorio!
            conn.execute(
                "UPDATE studenti SET nome=?, cognome=?, anno=? WHERE id=?",
                (request.form["nome"],
                 request.form["cognome"],
                 request.form["anno"],
                 id_studente)
            )
            return redirect(url_for("lista_studenti"))

        # GET: carica dati esistenti per pre-popolare il form
        studente = conn.execute(
            "SELECT * FROM studenti WHERE id = ?",
            (id_studente,)
        ).fetchone()

    return render_template("modifica.html", s=studente)
// pattern POST-Redirect-GET

Dopo ogni operazione DML (INSERT, UPDATE, DELETE) si usa redirect(url_for(...)) per tornare alla lista. Questo evita il problema del “doppio invio” se l’utente aggiorna la pagina dopo un POST (il browser non ri-invia i dati del form).

Riepilogo sicurezza: checklist per un’app DB-web

RischioSoluzioneIn Python
SQL InjectionPrepared statement sempreexecute(sql, (param,))
Password in chiaroHash con saltwerkzeug.security.generate_password_hash
XSS (Cross-Site Scripting)Escape automatico dell’outputJinja2 fa l’escape di default con {{ var }}
CSRF (Cross-Site Request Forgery)Token CSRF nei formFlask-WTF o token manuale in sessione
Dati sensibili in gitFile .env + .gitignorepython-dotenv, mai hardcoded secrets
📌 Riepilogo — Punti chiave
  • Il modulo sqlite3 è nella libreria standard Python: nessuna installazione, un solo file .db, perfetto per sviluppo e prototipazione
  • Il ciclo tipico è: connect()cursor()execute()fetchall()commit()close(). Meglio usare il context manager with
  • SQL Injection: attacco che inietta SQL nell’input per alterare le query. Causa data leak, accessi non autorizzati, perdita dati
  • Prevenzione: sempre prepared statement con ? come segnaposto. Mai concatenare stringhe per costruire query SQL
  • Flask permette di collegare form HTML a query SQL in poche righe: route GET per mostrare dati, route POST per riceverli e inserirli/aggiornarli
  • Jinja2 genera HTML dinamico con {% for %} e {{ variabile }}; il pattern POST-Redirect-GET previene il doppio invio del form

Lascia un commento