Condividi tramite


SQL injection

Si applica a: SQL Server Database SQL di Azure Istanza gestita di SQL di Azure Azure Synapse Analytics Piattaforma di strumenti analitici (PDW)

L'SQL injection è un attacco in cui il codice dannoso viene inserito in stringhe che vengono successivamente passate a un'istanza del Motore di database di SQL Server per l'analisi e l'esecuzione. Qualsiasi procedura che costruisce istruzioni SQL deve essere esaminata per individuare eventuali vulnerabilità di tipo injection, poiché il motore di database esegue tutte le query sintatticamente valide ricevute. Anche i dati con parametri possono essere modificati da un utente malintenzionato abile e determinato.

Come funziona un attacco SQL injection

La forma principale di un attacco intrusivo nel codice SQL consiste nell'inserimento diretto di codice in variabili di input utente concatenate a comandi SQL ed eseguite. Una forma meno diretta di attacco consiste nell'inserimento di malware in stringhe destinate all'archiviazione in una tabella o come metadati. Quando le stringhe archiviate vengono successivamente concatenate in un comando SQL dinamico, il codice dannoso viene eseguito.

Il processo di intrusione termina prematuramente una stringa di testo e aggiunge un nuovo comando. Poiché al comando inserito potrebbero essere aggiunte stringhe aggiuntive prima dell'esecuzione, l'utente malintenzionato termina la stringa inserita con un segno di commento --. Al momento del'esecuzione, il testo successivo al segno di commento viene ignorato.

Nello script seguente viene illustrata una semplice intrusione nel codice SQL. Lo script compila una query SQL concatenando stringhe hardcoded insieme a una stringa immessa dall'utente:

var ShipCity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

L'utente riceve la richiesta di immettere il nome di una città. Se immette Redmond, la query assemblata dallo script sarà simile al seguente esempio:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

Tuttavia, supponiamo che l'utente immetta il seguente testo:

Redmond';drop table OrdersTable--

In questo caso, lo script assembla la seguente query:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'

Il punto e virgola (;) indica la fine di una query e l'inizio di un'altra. Il doppio trattino (--) indica che il resto della riga corrente è un commento che deve essere ignorato. Se il codice modificato è sintatticamente corretto, verrà eseguito dal server. Quando il motore di database elabora questa istruzione, seleziona prima tutti i record in OrdersTable in cui ShipCity è Redmond. Quindi, il motore di database elimina OrdersTable.

Se il codice SQL inserito è sintatticamente corretto, le manomissioni non possono essere rilevate a livello di programmazione. È pertanto necessario convalidare tutti gli input utente e rivedere attentamente il codice per l'esecuzione dei comandi SQL generati nel server in uso. Nelle sezioni seguenti di questo articolo vengono illustrate le procedure consigliate relative al codice.

Convalidare tutti gli input

Convalidare sempre gli input utente testando tipo, lunghezza, formato e intervallo. Durante l'implementazione di precauzioni contro input dannosi, valutare gli scenari di distribuzione e l'architettura dell'applicazione in uso. È importante ricordare che i programmi destinati all'esecuzione in un ambiente sicuro possono essere copiati in un ambiente non sicuro. I suggerimenti seguenti vanno considerati procedure consigliate:

  • Non basarsi su presupposti relativi a dimensioni, tipo o contenuto dei dati ricevuti dall'applicazione. È ad esempio possibile valutare:

    • Come si comporta l'applicazione se un utente immette volontariamente o per errore un file video da 2 GB nel punto in cui è previsto un codice postale?

    • Come si comporta l'applicazione se un'istruzione DROP TABLE viene incorporata in un campo di testo?

  • Testare le dimensioni e il tipo di dati dell'input e imporre limiti appropriati. In questo modo è possibile impedire intenzionali sovraccarichi del buffer.

  • Testare il contenuto delle variabili stringa e accettare solo i valori previsti. Rifiutare voci contenenti dati binari, sequenze di escape e caratteri di commento. Ciò consente di evitare l'attacco intrusivo nel codice script e può fornire protezione da alcuni exploit basati sul sovraccarico del buffer.

  • Quando si utilizzano documenti XML, convalidare tutti i dati in base al relativo schema man mano che vengono immessi.

  • Non creare mai istruzioni Transact-SQL direttamente dall'input utente.

  • Utilizzare stored procedure per la convalida dell'input utente.

  • In ambienti multilivello è necessario convalidare tutti i dati prima di consentirne l'inserimento in aree considerate attendibili. I dati che non superano il processo di convalida devono essere rifiutati e un errore deve essere restituito al livello precedente.

  • Implementare più livelli di convalida. Le precauzioni possono risultare inefficaci nella difesa da utenti malintenzionati determinati. Una procedura consigliata consiste nel convalidare l'input nell'interfaccia utente e in tutti i successivi punti in cui l'input oltrepassa il limite di un'area attendibile.

    Ad esempio, la convalida dei dati in un'applicazione sul lato client può impedire i semplici attacchi intrusivi nel codice script. Se, tuttavia, al livello successivo si presuppone che il relativo input sia già stato convalidato, l'utente malintenzionato in grado di ignorare un client potrà accedere in modo illimitato a un sistema.

  • Non eseguire mai il concatenamento di input utente non convalidato. La concatenazione delle stringhe è il punto di ingresso principale per l'attacco intrusivo nel codice script.

  • Non accettare le stringhe seguenti nei campi da cui è possibile costruire i nomi di file: AUX, CLOCK$, COM1 fino a COM8, CON, CONFIG$, LPT1 fino a LPT8, NUL e PRN.

Quando possibile, rifiutare l'input contenente i caratteri seguenti.

Carattere di input Significato in Transact-SQL
; Delimitatore di query
' Delimitatore di stringhe di dati di tipo carattere
-- Delimitatore di commento a riga singola. Il testo che segue -- fino alla fine della riga non viene valutato dal server.
/*** ... ***/ Delimitatori di commento. Il testo compreso fra /* e */ non viene valutato dal server.
xp_ Usato all'inizio del nome delle stored procedure estese di catalogo come xp_cmdshell.

Usare parametri SQL indipendenti dai tipi

La raccolta Parameters nel motore di database implementa il controllo del tipo e la convalida della lunghezza. Se si usa la raccolta Parameters, l'input viene interpretato come valore letterale e non come codice eseguibile. Un ulteriore vantaggio dell'utilizzo della raccolta Parameters consiste nella possibilità di applicare controlli sul tipo e sulla lunghezza. I valori non compresi nell'intervallo attivano un'eccezione. Nel frammento di codice seguente viene illustrato l'utilizzo della raccolta Parameters:

SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

In questo esempio il parametro @au_id viene interpretato come valore letterale e non come codice eseguibile. Vengono controllati il tipo e la lunghezza del valore. Se il valore di @au_id non è conforme ai vincoli di tipo e lunghezza specificati, viene generata un'eccezione.

Usare input con parametri nelle stored procedure

Le stored procedure sono vulnerabili all'attacco intrusivo nel codice SQL se utilizzano input non filtrato. Ad esempio, il codice seguente è vulnerabile:

SqlDataAdapter myCommand =
    new SqlDataAdapter("LoginStoredProcedure '" + Login.Text + "'", conn);

Se si utilizzano stored procedure è opportuno utilizzare come input solo parametri.

Usare la raccolta Parametri nelle istruzioni SQL dinamiche

Se non si possono usare stored procedure, è comunque possibile usare parametri, come mostrato nell'esempio di codice seguente.

SqlDataAdapter myCommand = new SqlDataAdapter(
    "SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn);
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

Filtrare l'input

È anche possibile filtrare l'input per proteggersi da attacchi intrusivi nel codice SQL tramite la rimozione di caratteri di escape. Dato il numero elevato di caratteri che potrebbero causare problemi, non è tuttavia possibile considerare affidabile questo metodo di difesa. Nell'esempio seguente viene eseguita la ricerca del delimitatore di stringhe di caratteri.

private string SafeSqlLiteral(string inputSQL)
{
    return inputSQL.Replace("'", "''");
}

Clausole LIKE

Se si usa una clausola LIKE sarà comunque necessario usare caratteri di escape per i caratteri jolly:

s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");

Esaminare il codice per attacchi intrusivi nel codice SQL

È necessario rivedere tutto il codice che chiama EXECUTE, EXECo sp_executesql. È possibile utilizzare query simili alla seguente per identificare procedure contenenti queste istruzioni. Questa query verifica la presenza di 1, 2, 3 o 4 spazi dopo le parole EXECUTE o EXEC.

SELECT object_Name(id)
FROM syscomments
WHERE UPPER(TEXT) LIKE '%EXECUTE (%'
    OR UPPER(TEXT) LIKE '%EXECUTE  (%'
    OR UPPER(TEXT) LIKE '%EXECUTE   (%'
    OR UPPER(TEXT) LIKE '%EXECUTE    (%'
    OR UPPER(TEXT) LIKE '%EXEC (%'
    OR UPPER(TEXT) LIKE '%EXEC  (%'
    OR UPPER(TEXT) LIKE '%EXEC   (%'
    OR UPPER(TEXT) LIKE '%EXEC    (%'
    OR UPPER(TEXT) LIKE '%SP_EXECUTESQL%';

Eseguire il wrapping di parametri con QUOTENAME() e REPLACE()

In ogni stored procedure selezionata verificare che tutte le variabili usate in Transact-SQL dinamico vengano gestite correttamente. Il wrapping dei dati provenienti dai parametri di input della stored procedure o letti da una tabella deve essere eseguito in QUOTENAME() o REPLACE(). Ricordare che il valore di @variable passato a QUOTENAME() è di tipo sysname e presenta una lunghezza massima di 128 caratteri.

@variable Wrapper consigliato
Nome di un'entità a protezione diretta QUOTENAME(@variable)
Stringa di <= 128 caratteri QUOTENAME(@variable, '''')
Stringa di > 128 caratteri REPLACE(@variable,'''', '''''')

Quando si utilizza questa tecnica, è possibile rivedere un'istruzione SET nel modo seguente:

-- Before:
SET @temp = N'SELECT * FROM authors WHERE au_lname ='''
    + @au_lname + N'''';

-- After:
SET @temp = N'SELECT * FROM authors WHERE au_lname = '''
    + REPLACE(@au_lname, '''', '''''') + N'''';

Attacco intrusivo consentito dal troncamento dei dati

Qualunque codice Transact-SQL dinamico assegnato a una variabile verrà troncato se è maggiore del buffer allocato per tale variabile. Un utente malintenzionato in grado di imporre il troncamento delle istruzioni passando stringhe inaspettatamente lunghe a una stored procedure può modificare il risultato. La stored procedure creata dallo script seguente, ad esempio, è vulnerabile agli attacchi intrusivi consentiti dal troncamento.

In questo esempio è presente un buffer @command con una lunghezza massima di 200 caratteri. È necessario un totale di 154 caratteri per impostare la password di 'sa': 26 per l'istruzione UPDATE , 16 per la clausola WHERE, 4 per 'sa' e 2 per le virgolette racchiuse tra QUOTENAME(@loginname): 200 - 26 - 16 - 4 - 2 = 154. Tuttavia, poiché @new è dichiarato come sysname, questa variabile può contenere solo 128 caratteri. È possibile risolvere questo problema passando alcune virgolette singole in @new.

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variable.
DECLARE @command VARCHAR(200)

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password=' + QUOTENAME(@new, '''')
    + ' WHERE username=' + QUOTENAME(@loginname, '''') 
    + ' AND password=' + QUOTENAME(@old, '''')

-- Execute the command.
EXEC (@command);
GO

Passando 154 caratteri in un buffer di 128 caratteri, un utente malintenzionato può impostare una nuova password per sa senza conoscere quella vecchia.

EXEC sp_MySetPassword 'sa',
    'dummy',
    '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''

Per questo motivo, usare un buffer di grandi dimensioni per una variabile di comando oppure eseguire codice Transact-SQL dinamico direttamente all'interno dell’istruzione EXECUTE.

Troncamento se vengono utilizzati QUOTENAME(@variable, '''') e REPLACE()

Le stringhe restituite da QUOTENAME() e REPLACE() verranno troncate senza avviso se superano lo spazio allocato. La stored procedure creata nell'esempio seguente illustra le conseguenze.

In questo esempio i dati archiviati nelle variabili temporanee vengono troncati, perché la dimensione buffer di @login, @oldpassword e @newpassword è di soli 128 caratteri, ma QUOTENAME() può restituire fino a 258 caratteri. Se @new contiene 128 caratteri, allora @newpassword può essere 123... n, dove n è il 127° carattere. Poiché la stringa restituita da QUOTENAME() viene troncata, può essere eseguita come nell'istruzione seguente:

UPDATE Users SET password ='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = QUOTENAME(@loginname, '''');
SET @oldpassword = QUOTENAME(@old, '''');
SET @newpassword = QUOTENAME(@new, '''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users set password = ' + @newpassword
    + ' WHERE username = ' + @login
    + ' AND password = ' + @oldpassword;

-- Execute the command.
EXEC (@command);
GO

Di conseguenza, l'istruzione seguente imposterà le password di tutti gli utenti sul valore passato nel codice precedente.

EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'

È possibile forzare il troncamento delle stringhe superando lo spazio del buffer allocato quando si utilizza REPLACE(). La stored procedure creata nell'esempio seguente illustra le conseguenze.

In questo esempio i dati vengono troncati perché i buffer allocati per @login, @oldpassword e @newpassword possono contenere solo 128 caratteri, ma QUOTENAME() può restituire fino a 258 caratteri. Se @new contiene 128 caratteri, @newpassword può essere '123...n', dove n è il 127° carattere. Poiché la stringa restituita da QUOTENAME() viene troncata, può essere eseguita come nell'istruzione seguente:

UPDATE Users SET password='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = REPLACE(@loginname, '''', '''''');
SET @oldpassword = REPLACE(@old, '''', '''''');
SET @newpassword = REPLACE(@new, '''', '''''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password = '''
    + @newpassword + ''' WHERE username = '''
    + @login + ''' AND password = ''' + @oldpassword + '''';

-- Execute the command.
EXEC (@command);
GO

Come con QUOTENAME(), il troncamento delle stringhe da parte di REPLACE() può essere evitato dichiarando variabili temporanee sufficientemente lunghe per tutti i casi. Quando possibile, è necessario chiamare QUOTENAME() o REPLACE() direttamente all'interno del codice Transact-SQL dinamico. In caso contrario, è possibile calcolare la dimensione richiesta del buffer nel modo seguente. Per @outbuffer = QUOTENAME(@input), la dimensione di @outbuffer deve essere 2 * (len(@input) + 1). Quando si usa REPLACE() e le virgolette doppie, come nell'esempio precedente, un buffer di 2 * len(@input) è sufficiente.

Il calcolo seguente tratta tutti i casi:

WHILE LEN(@find_string) > 0, required buffer size =
    ROUND(LEN(@input) / LEN(@find_string), 0)
        * LEN(@new_string) + (LEN(@input) % LEN(@find_string))

Troncamento quando viene utilizzato QUOTENAME(@variable, ']')

Può verificarsi il troncamento quando il nome di un'entità a protezione diretta del motore di database viene passato a istruzioni che utilizzano il formato QUOTENAME(@variable, ']'). L'esempio seguente illustra questo scenario.

In questo esempio, @objectname deve consentire 2 * 258 + 1 caratteri.

CREATE PROCEDURE sp_MyProc
    @schemaname SYSNAME,
    @tablename SYSNAME
AS
-- Declare a variable as sysname. The variable will be 128 characters.
DECLARE @objectname SYSNAME;

SET @objectname = QUOTENAME(@schemaname) + '.' + QUOTENAME(@tablename);
    -- Do some operations.
GO

Quando si concatenano valori di tipo sysname, è consigliabile usare variabili temporanee di lunghezza sufficiente per contenere al massimo 128 caratteri per valore. Se possibile, chiamare QUOTENAME() direttamente all'interno del codice Transact-SQL dinamico. In caso contrario, è possibile calcolare la dimensione richiesta del buffer, come illustrato nella sezione precedente.