Database
 sql >> Database >  >> RDS >> Database

Prevenire gli attacchi di SQL injection con Python

Ogni pochi anni, l'Open Web Application Security Project (OWASP) classifica i rischi più critici per la sicurezza delle applicazioni Web. Dal primo rapporto, i rischi di iniezione sono sempre stati al primo posto. Tra tutti i tipi di injection, SQL injection è uno dei vettori di attacco più comuni e probabilmente il più pericoloso. Poiché Python è uno dei linguaggi di programmazione più popolari al mondo, sapere come proteggersi dall'iniezione SQL di Python è fondamentale.

In questo tutorial imparerai:

  • Cosa Iniezione SQL Python è e come prevenirlo
  • Come comporre query con sia i valori letterali che gli identificatori come parametri
  • Come eseguire query in sicurezza in un database

Questo tutorial è adatto a utenti di tutti i motori di database . Gli esempi qui utilizzano PostgreSQL, ma i risultati possono essere riprodotti in altri sistemi di gestione di database (come SQLite, MySQL, Microsoft SQL Server, Oracle e così via).

Bonus gratuito: 5 pensieri su Python Mastery, un corso gratuito per sviluppatori Python che ti mostra la roadmap e la mentalità di cui avrai bisogno per portare le tue abilità in Python al livello successivo.


Capire Python SQL injection

Gli attacchi SQL Injection sono una vulnerabilità di sicurezza così comune che il leggendario xkcd webcomic gli ha dedicato un fumetto:

La generazione e l'esecuzione di query SQL è un'attività comune. Tuttavia, le aziende di tutto il mondo spesso commettono errori orribili quando si tratta di comporre istruzioni SQL. Mentre il livello ORM di solito compone query SQL, a volte devi scriverne di tue.

Quando usi Python per eseguire queste query direttamente in un database, è possibile che tu possa commettere errori che potrebbero compromettere il tuo sistema. In questo tutorial imparerai come implementare con successo funzioni che compongono query SQL dinamiche senza mettendo il tuo sistema a rischio per Python SQL injection.



Impostazione di un database

Per iniziare, configurerai un nuovo database PostgreSQL e lo compilerai con i dati. Durante il tutorial, utilizzerai questo database per testimoniare in prima persona come funziona Python SQL injection.


Creazione di un database

Per prima cosa, apri la tua shell e crea un nuovo database PostgreSQL di proprietà dell'utente postgres :

$ createdb -O postgres psycopgtest

Qui hai usato l'opzione della riga di comando -O per impostare il proprietario del database sull'utente postgres . Hai anche specificato il nome del database, che è psycopgtest .

Nota: postgres è un utente speciale , che normalmente riserveresti per attività amministrative, ma per questo tutorial va bene usare postgres . In un sistema reale, invece, dovresti creare un utente separato che sia il proprietario del database.

Il tuo nuovo database è pronto per partire! Puoi connetterti usando psql :

$ psql -U postgres -d psycopgtest
psql (11.2, server 10.5)
Type "help" for help.

Ora sei connesso al database psycopgtest come utente postgres . Questo utente è anche il proprietario del database, quindi avrai i permessi di lettura su ogni tabella del database.



Creazione di una tabella con dati

Successivamente, devi creare una tabella con alcune informazioni sull'utente e aggiungervi dati:

psycopgtest=# CREATE TABLE users (
    username varchar(30),
    admin boolean
);
CREATE TABLE

psycopgtest=# INSERT INTO users
    (username, admin)
VALUES
    ('ran', true),
    ('haki', false);
INSERT 0 2

psycopgtest=# SELECT * FROM users;
 username | admin
----------+-------
 ran      | t
 haki     | f
(2 rows)

La tabella ha due colonne:username e admin . L'admin la colonna indica se un utente dispone o meno dei privilegi di amministratore. Il tuo obiettivo è scegliere come target l'admin campo e prova ad abusarne.



Configurazione di un ambiente virtuale Python

Ora che hai un database, è il momento di configurare il tuo ambiente Python. Per istruzioni dettagliate su come farlo, dai un'occhiata a Python Virtual Environments:A Primer.

Crea il tuo ambiente virtuale in una nuova directory:

(~/src) $ mkdir psycopgtest
(~/src) $ cd psycopgtest
(~/src/psycopgtest) $ python3 -m venv venv

Dopo aver eseguito questo comando, una nuova directory chiamata venv verrà creato. Questa directory memorizzerà tutti i pacchetti installati all'interno dell'ambiente virtuale.



Connessione al database

Per connettersi a un database in Python, è necessario un adattatore per database . La maggior parte degli adattatori di database segue la versione 2.0 della specifica API del database Python PEP 249. Ogni motore di database principale ha un adattatore principale:

Database Adattatore
PostgreSQL Psicoagulo
SQLite sqlite3
Oracolo cx_oracle
MySql MySQLdb

Per connetterti a un database PostgreSQL, dovrai installare Psycopg, che è l'adattatore più popolare per PostgreSQL in Python. Django ORM lo utilizza per impostazione predefinita ed è supportato anche da SQLAlchemy.

Nel tuo terminale, attiva l'ambiente virtuale e usa pip per installare psycopg :

(~/src/psycopgtest) $ source venv/bin/activate
(~/src/psycopgtest) $ python -m pip install psycopg2>=2.8.0
Collecting psycopg2
  Using cached https://....
  psycopg2-2.8.2.tar.gz
Installing collected packages: psycopg2
  Running setup.py install for psycopg2 ... done
Successfully installed psycopg2-2.8.2

Ora sei pronto per creare una connessione al tuo database. Ecco l'inizio del tuo script Python:

import psycopg2

connection = psycopg2.connect(
    host="localhost",
    database="psycopgtest",
    user="postgres",
    password=None,
)
connection.set_session(autocommit=True)

Hai usato psycopg2.connect() per creare la connessione. Questa funzione accetta i seguenti argomenti:

  • host è l'indirizzo IP o il DNS del server in cui si trova il database. In questo caso, l'host è la tua macchina locale, o localhost .

  • database è il nome del database a cui connettersi. Vuoi connetterti al database che hai creato in precedenza, psycopgtest .

  • user è un utente con autorizzazioni per il database. In questo caso, vuoi connetterti al database come proprietario, quindi passi all'utente postgres .

  • password è la password per chi hai specificato in user . Nella maggior parte degli ambienti di sviluppo, gli utenti possono connettersi al database locale senza password.

Dopo aver impostato la connessione, hai configurato la sessione con autocommit=True . Attivazione di autocommit significa che non dovrai gestire manualmente le transazioni emettendo un commit o rollback . Questo è il comportamento predefinito della maggior parte degli ORM. Anche qui utilizzi questo comportamento in modo da poterti concentrare sulla composizione di query SQL anziché sulla gestione delle transazioni.

Nota: Gli utenti Django possono ottenere l'istanza della connessione utilizzata dall'ORM da django.db.connection :

from django.db import connection


Esecuzione di una query

Ora che hai una connessione al database, sei pronto per eseguire una query:

>>>
>>> with connection.cursor() as cursor:
...     cursor.execute('SELECT COUNT(*) FROM users')
...     result = cursor.fetchone()
... print(result)
(2,)

Hai usato la connection oggetto per creare un cursor . Proprio come un file in Python, cursor viene implementato come gestore di contesto. Quando crei il contesto, un cursor viene aperto per essere utilizzato per inviare comandi al database. Quando il contesto esce, il cursor si chiude e non puoi più usarlo.

Nota: Per saperne di più sui gestori di contesto, dai un'occhiata ai gestori di contesto Python e alla dichiarazione "with".

All'interno del contesto, hai usato cursor per eseguire una query e recuperare i risultati. In questo caso, hai eseguito una query per contare le righe negli users tavolo. Per recuperare il risultato dalla query, hai eseguito cursor.fetchone() e ha ricevuto una tupla. Poiché la query può restituire un solo risultato, hai utilizzato fetchone() . Se la query dovesse restituire più di un risultato, dovresti eseguire un'iterazione su cursor oppure usa uno degli altri fetch* metodi.




Utilizzo dei parametri di query in SQL

Nella sezione precedente, hai creato un database, stabilito una connessione ad esso ed eseguito una query. La query che hai utilizzato era statica . In altre parole, non aveva nessun parametro . Ora inizierai a utilizzare i parametri nelle tue query.

Innanzitutto, implementerai una funzione che verifica se un utente è un amministratore o meno. is_admin() accetta un nome utente e restituisce lo stato di amministratore dell'utente:

# BAD EXAMPLE. DON'T DO THIS!
def is_admin(username: str) -> bool:
    with connection.cursor() as cursor:
        cursor.execute("""
            SELECT
                admin
            FROM
                users
            WHERE
                username = '%s'
        """ % username)
        result = cursor.fetchone()
    admin, = result
    return admin

Questa funzione esegue una query per recuperare il valore di admin colonna per un determinato nome utente. Hai usato fetchone() per restituire una tupla con un unico risultato. Quindi, hai decompresso questa tupla nella variabile admin . Per testare la tua funzione, controlla alcuni nomi utente:

>>>
>>> is_admin('haki')
False
>>> is_admin('ran')
True

Fin qui tutto bene. La funzione ha restituito il risultato previsto per entrambi gli utenti. Ma per quanto riguarda l'utente inesistente? Dai un'occhiata a questo traceback di Python:

>>>
>>> is_admin('foo')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 12, in is_admin
TypeError: cannot unpack non-iterable NoneType object

Quando l'utente non esiste, un TypeError è sollevato. Questo perché .fetchone() restituisce None quando non vengono trovati risultati e scompattando None genera un TypeError . L'unico posto in cui puoi decomprimere una tupla è dove inserisci admin da result .

Per gestire utenti inesistenti, crea un caso speciale per quando result è None :

# BAD EXAMPLE. DON'T DO THIS!
def is_admin(username: str) -> bool:
    with connection.cursor() as cursor:
        cursor.execute("""
            SELECT
                admin
            FROM
                users
            WHERE
                username = '%s'
        """ % username)
        result = cursor.fetchone()

    if result is None:
        # User does not exist
        return False

    admin, = result
    return admin

Qui hai aggiunto un caso speciale per la gestione di None . Se username non esiste, la funzione dovrebbe restituire False . Ancora una volta, testa la funzione su alcuni utenti:

>>>
>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False

Grande! La funzione ora può gestire anche nomi utente inesistenti.



Utilizzo dei parametri di query con Python SQL injection

Nell'esempio precedente è stata utilizzata l'interpolazione di stringhe per generare una query. Quindi, hai eseguito la query e inviato la stringa risultante direttamente al database. Tuttavia, c'è qualcosa che potresti aver trascurato durante questo processo.

Ripensa al username argomento passato a is_admin() . Cosa rappresenta esattamente questa variabile? Potresti presumere che username è solo una stringa che rappresenta il nome di un utente effettivo. Come stai per vedere, tuttavia, un intruso può facilmente sfruttare questo tipo di svista e causare gravi danni eseguendo l'iniezione SQL di Python.

Prova a verificare se il seguente utente è un amministratore o meno:

>>>
>>> is_admin("'; select true; --")
True

Aspetta... Cos'è appena successo?

Diamo un'altra occhiata all'implementazione. Stampa la query effettiva in esecuzione nel database:

>>>
>>> print("select admin from users where username = '%s'" % "'; select true; --")
select admin from users where username = ''; select true; --'

Il testo risultante contiene tre affermazioni. Per capire esattamente come funziona Python SQL injection, è necessario ispezionare ogni parte individualmente. La prima affermazione è la seguente:

select admin from users where username = '';

Questa è la tua domanda prevista. Il punto e virgola (; ) termina la query, quindi il risultato di questa query non ha importanza. La prossima è la seconda affermazione:

select true;

Questa affermazione è stata costruita dall'intruso. È progettato per restituire sempre True .

Infine, vedi questo breve pezzo di codice:

--'

Questo snippet disinnesca tutto ciò che viene dopo. L'intruso ha aggiunto il simbolo del commento (-- ) per trasformare tutto ciò che potresti aver inserito dopo l'ultimo segnaposto in un commento.

Quando esegui la funzione con questo argomento, restituirà sempre True . Se, ad esempio, utilizzi questa funzione nella tua pagina di login, un intruso potrebbe accedere con lo username '; select true; -- e gli verrà concesso l'accesso.

Se pensi che questo sia un male, potrebbe peggiorare! Gli intrusi con conoscenza della struttura della tua tabella possono utilizzare Python SQL injection per causare danni permanenti. Ad esempio, l'intruso può inserire una dichiarazione di aggiornamento per alterare le informazioni nel database:

>>>
>>> is_admin('haki')
False
>>> is_admin("'; update users set admin = 'true' where username = 'haki'; select true; --")
True
>>> is_admin('haki')
True

Analizziamolo di nuovo:

';

Questo frammento di codice termina la query, proprio come nell'iniezione precedente. La prossima affermazione è la seguente:

update users set admin = 'true' where username = 'haki';

Questa sezione aggiorna admin su true per l'utente haki .

Infine, c'è questo frammento di codice:

select true; --

Come nell'esempio precedente, questo pezzo restituisce true e commenta tutto ciò che segue.

Perché è peggio? Bene, se l'intruso riesce ad eseguire la funzione con questo input, allora l'utente haki diventerà un amministratore:

psycopgtest=# select * from users;
 username | admin
----------+-------
 ran      | t
 haki     | t
(2 rows)

L'intruso non deve più usare l'hack. Possono semplicemente accedere con il nome utente haki . (Se l'intruso davvero voleva causare danni, quindi potrebbero persino emettere un DROP DATABASE comando.)

Prima di dimenticare, ripristina haki al suo stato originale:

psycopgtest=# update users set admin = false where username = 'haki';
UPDATE 1

Allora, perché sta succedendo questo? Bene, cosa sai del username discussione? Sai che dovrebbe essere una stringa che rappresenta il nome utente, ma in realtà non controlli o applichi questa affermazione. Questo può essere pericoloso! È esattamente ciò che cercano gli aggressori quando tentano di hackerare il tuo sistema.


Creazione di parametri di query sicure

Nella sezione precedente, hai visto come un intruso può sfruttare il tuo sistema e ottenere i permessi di amministratore utilizzando una stringa accuratamente predisposta. Il problema era che consentivi l'esecuzione del valore passato dal client direttamente nel database, senza eseguire alcun tipo di controllo o convalida. Le iniezioni SQL si basano su questo tipo di vulnerabilità.

Ogni volta che l'input dell'utente viene utilizzato in una query del database, esiste una possibile vulnerabilità per SQL injection. La chiave per prevenire l'iniezione SQL di Python è assicurarsi che il valore venga utilizzato come previsto dallo sviluppatore. Nell'esempio precedente, intendevi username da usare come stringa. In realtà, è stato utilizzato come un'istruzione SQL grezza.

Per assicurarti che i valori vengano utilizzati come previsto, devi escape il valore. Ad esempio, per impedire agli intrusi di iniettare SQL non elaborato al posto di un argomento stringa, puoi evitare le virgolette:

>>>
>>> # BAD EXAMPLE. DON'T DO THIS!
>>> username = username.replace("'", "''")

Questo è solo un esempio. Ci sono molti caratteri speciali e scenari a cui pensare quando si cerca di prevenire l'iniezione SQL di Python. Fortunatamente per te, i moderni adattatori di database sono dotati di strumenti integrati per prevenire l'iniezione SQL di Python utilizzando parametri di query . Questi vengono utilizzati al posto della semplice interpolazione di stringhe per comporre una query con parametri.

Nota: Adattatori, database e linguaggi di programmazione diversi fanno riferimento a parametri di query con nomi diversi. I nomi comuni includono variabili di associazione , variabili di sostituzione e variabili di sostituzione .

Ora che hai una migliore comprensione della vulnerabilità, sei pronto per riscrivere la funzione utilizzando i parametri di query anziché l'interpolazione di stringhe:

 1def is_admin(username: str) -> bool:
 2    with connection.cursor() as cursor:
 3        cursor.execute("""
 4            SELECT
 5                admin
 6            FROM
 7                users
 8            WHERE
 9                username = %(username)s
10        """, {
11            'username': username
12        })
13        result = cursor.fetchone()
14
15    if result is None:
16        # User does not exist
17        return False
18
19    admin, = result
20    return admin

Ecco cosa c'è di diverso in questo esempio:

  • Nella riga 9, hai usato un parametro denominato username per indicare dove dovrebbe andare il nome utente. Nota come il parametro username non è più racchiuso tra virgolette singole.

  • Nella riga 11, hai passato il valore di username come secondo argomento di cursor.execute() . La connessione utilizzerà il tipo e il valore di username durante l'esecuzione della query nel database.

Per testare questa funzione, prova alcuni valori validi e non validi, inclusa la stringa pericolosa di prima:

>>>
>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False
>>> is_admin("'; select true; --")
False

Sorprendente! La funzione ha restituito il risultato previsto per tutti i valori. Inoltre, la stringa pericolosa non funziona più. Per capire perché, puoi esaminare la query generata da execute() :

>>>
>>> with connection.cursor() as cursor:
...    cursor.execute("""
...        SELECT
...            admin
...        FROM
...            users
...        WHERE
...            username = %(username)s
...    """, {
...        'username': "'; select true; --"
...    })
...    print(cursor.query.decode('utf-8'))
SELECT
    admin
FROM
    users
WHERE
    username = '''; select true; --'

La connessione ha trattato il valore di username come stringa ed è sfuggito a tutti i caratteri che potrebbero terminare la stringa e introdurre l'iniezione SQL di Python.



Passo dei parametri delle query sicure

Gli adattatori di database in genere offrono diversi modi per passare i parametri di query. Segnaposto con nome sono generalmente i migliori per la leggibilità, ma alcune implementazioni potrebbero trarre vantaggio dall'utilizzo di altre opzioni.

Diamo una rapida occhiata ad alcuni dei modi giusti e sbagliati per utilizzare i parametri di query. Il seguente blocco di codice mostra i tipi di query che vorrai evitare:

# BAD EXAMPLES. DON'T DO THIS!
cursor.execute("SELECT admin FROM users WHERE username = '" + username + '");
cursor.execute("SELECT admin FROM users WHERE username = '%s' % username);
cursor.execute("SELECT admin FROM users WHERE username = '{}'".format(username));
cursor.execute(f"SELECT admin FROM users WHERE username = '{username}'");

Ognuna di queste istruzioni passa username dal cliente direttamente al database, senza effettuare alcun tipo di verifica o validazione. Questo tipo di codice è maturo per invitare Python SQL injection.

Al contrario, questi tipi di query dovrebbero essere sicuri da eseguire:

# SAFE EXAMPLES. DO THIS!
cursor.execute("SELECT admin FROM users WHERE username = %s'", (username, ));
cursor.execute("SELECT admin FROM users WHERE username = %(username)s", {'username': username});

In queste istruzioni, username viene passato come parametro denominato. Ora, il database utilizzerà il tipo e il valore specificati di username durante l'esecuzione della query, offrendo protezione dall'iniezione SQL di Python.




Utilizzo della composizione SQL

Finora hai usato i parametri per i letterali. Valori letterali sono valori come numeri, stringhe e date. Ma cosa succede se si dispone di un caso d'uso che richiede la composizione di una query diversa, in cui il parametro è qualcos'altro, come il nome di una tabella o di una colonna?

Ispirandosi all'esempio precedente, implementiamo una funzione che accetta il nome di una tabella e restituisce il numero di righe in quella tabella:

# BAD EXAMPLE. DON'T DO THIS!
def count_rows(table_name: str) -> int:
    with connection.cursor() as cursor:
        cursor.execute("""
            SELECT
                count(*)
            FROM
                %(table_name)s
        """, {
            'table_name': table_name,
        })
        result = cursor.fetchone()

    rowcount, = result
    return rowcount

Prova ad eseguire la funzione sulla tabella dei tuoi utenti:

>>>
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in count_rows
psycopg2.errors.SyntaxError: syntax error at or near "'users'"
LINE 5:                 'users'
                        ^

Il comando non è riuscito a generare l'SQL. Come hai già visto, l'adattatore del database tratta la variabile come una stringa o un valore letterale. Un nome di tabella, tuttavia, non è una semplice stringa. È qui che entra in gioco la composizione SQL.

Sai già che non è sicuro usare l'interpolazione di stringhe per comporre SQL. Fortunatamente, Psycopg fornisce un modulo chiamato psycopg.sql per aiutarti a comporre in modo sicuro query SQL. Riscriviamo la funzione usando psycopg.sql.SQL() :

from psycopg2 import sql

def count_rows(table_name: str) -> int:
    with connection.cursor() as cursor:
        stmt = sql.SQL("""
            SELECT
                count(*)
            FROM
                {table_name}
        """).format(
            table_name = sql.Identifier(table_name),
        )
        cursor.execute(stmt)
        result = cursor.fetchone()

    rowcount, = result
    return rowcount

Ci sono due differenze in questa implementazione. Innanzitutto, hai usato sql.SQL() per comporre la query. Quindi, hai usato sql.Identifier() per annotare il valore dell'argomento table_name . (Un identificatore è il nome di una colonna o di una tabella.)

Nota: Utenti del popolare pacchetto django-debug-toolbar potrebbe ricevere un errore nel pannello SQL per le query composte con psycopg.sql.SQL() . È prevista una correzione per il rilascio nella versione 2.0.

Ora prova ad eseguire la funzione sugli users tabella:

>>>
>>> count_rows('users')
2

Grande! Quindi, vediamo cosa succede quando la tabella non esiste:

>>>
>>> count_rows('foo')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in count_rows
psycopg2.errors.UndefinedTable: relation "foo" does not exist
LINE 5:                 "foo"
                        ^

La funzione genera la UndefinedTable eccezione. Nei passaggi seguenti, utilizzerai questa eccezione come indicazione che la tua funzione è al sicuro da un attacco Python SQL injection.

Nota: L'eccezione UndefinedTable è stato aggiunto in psycopg2 versione 2.8. Se stai lavorando con una versione precedente di Psycopg, otterrai un'eccezione diversa.

Per mettere tutto insieme, aggiungi un'opzione per contare le righe nella tabella fino a un certo limite. Questa funzione potrebbe essere utile per tabelle molto grandi. Per implementarlo, aggiungi un LIMIT clausola alla query, insieme ai parametri della query per il valore del limite:

from psycopg2 import sql

def count_rows(table_name: str, limit: int) -> int:
    with connection.cursor() as cursor:
        stmt = sql.SQL("""
            SELECT
                COUNT(*)
            FROM (
                SELECT
                    1
                FROM
                    {table_name}
                LIMIT
                    {limit}
            ) AS limit_query
        """).format(
            table_name = sql.Identifier(table_name),
            limit = sql.Literal(limit),
        )
        cursor.execute(stmt)
        result = cursor.fetchone()

    rowcount, = result
    return rowcount

In questo blocco di codice, hai annotato limit usando sql.Literal() . Come nell'esempio precedente, psycopg collegherà tutti i parametri della query come valori letterali quando si utilizza l'approccio semplice. Tuttavia, quando si utilizza sql.SQL() , è necessario annotare in modo esplicito ogni parametro utilizzando sql.Identifier() o sql.Literal() .

Nota: Sfortunatamente, la specifica dell'API Python non affronta il binding degli identificatori, ma solo i letterali. Psycopg è l'unico adattatore popolare che ha aggiunto la possibilità di comporre in modo sicuro SQL sia con letterali che con identificatori. Questo fatto rende ancora più importante prestare molta attenzione quando si associano gli identificatori.

Esegui la funzione per assicurarti che funzioni:

>>>
>>> count_rows('users', 1)
1
>>> count_rows('users', 10)
2

Ora che vedi che la funzione funziona, assicurati che sia anche sicura:

>>>
>>> count_rows("(select 1) as foo; update users set admin = true where name = 'haki'; --", 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in count_rows
psycopg2.errors.UndefinedTable: relation "(select 1) as foo; update users set admin = true where name = '" does not exist
LINE 8:                     "(select 1) as foo; update users set adm...
                            ^

Questa traccia mostra che psycopg è sfuggito al valore e il database lo ha trattato come un nome di tabella. Poiché una tabella con questo nome non esiste, una UndefinedTable è stata sollevata un'eccezione e non sei stato violato!



Conclusione

Hai implementato con successo una funzione che compone SQL dinamico senza mettendo il tuo sistema a rischio per Python SQL injection! Hai utilizzato sia valori letterali che identificatori nella tua query senza compromettere la sicurezza.

Hai imparato:

  • Cosa Iniezione SQL Python è e come può essere sfruttato
  • Come prevenire l'iniezione SQL di Python utilizzando i parametri di query
  • Come comporre in modo sicuro istruzioni SQL che utilizzano letterali e identificatori come parametri

Ora puoi creare programmi in grado di resistere ad attacchi dall'esterno. Vai avanti e contrasta gli hacker!