Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Si applica a:SQL Server
Database SQL di Azure
Istanza gestita di SQL di Azure
Azure Synapse Analytics
Piattaforma di analisi (PDW)
Endpoint di analisi SQL in Microsoft Fabric
Magazzino in Microsoft Fabric
Database SQL in Microsoft Fabric
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 iniezione funziona terminando prematuramente una stringa di testo e aggiungendo 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 --. Il testo successivo viene ignorato in fase di esecuzione.
Nello script seguente viene illustrata una semplice intrusione nel codice SQL. Lo script costruisce una query SQL concatenando stringhe codificate in modo statico con 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 TABLEviene 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 le stored procedure per convalidare l'input dell'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 che adotti contro utenti occasionalmente malintenzionati potrebbero rivelarsi inefficaci contro aggressori 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 concatenare mai input dell'utente non validato. 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$,COM1fino aCOM8,CON,CONFIG$,LPT1fino aLPT8,NULePRN.
Se possibile, rifiutare l'input contenente i caratteri seguenti.
| Carattere di input | Significato in Transact-SQL |
|---|---|
; |
Delimitatore di query. |
' |
Delimitatore di stringa di dati di caratteri |
-- |
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 del catalogo, come xp_cmdshell. |
Usa parametri SQL con controllo dei tipi
La raccolta Parameters nel Motore di database fornisce il controllo del tipo di dati 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. Questo valore viene verificato per tipo e lunghezza. 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 procedure memorizzate potrebbero essere vulnerabili alla SQL injection se utilizzano input non filtrati. 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 "Parameters" con SQL dinamico
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%';
Racchiudere i parametri tra QUOTENAME() e REPLACE()
In ogni stored procedure selezionata, verificare che tutte le variabili utilizzate in Transact-SQL dinamico vengano gestite correttamente. I dati provenienti dai parametri di input della stored procedure o letti da una tabella devono essere racchiusi 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 elemento proteggibile | 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 attaccante che riesce a forzare il troncamento di un'istruzione passando stringhe inaspettatamente lunghe a una procedura memorizzata può manipolare il risultato. Ad esempio, la stored procedure di esempio seguente è vulnerabile agli attacchi di inserimento resi possibili 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 procedura archiviata che viene creata nell'esempio seguente mostra cosa può accadere.
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 procedura memorizzata creata nell'esempio seguente mostra cosa può accadere.
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 si utilizza QUOTENAME(@variable, ']')
Può verificarsi un troncamento quando il nome di un oggetto a protezione diretta del Motore di database viene passato a istruzioni che usano la forma 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.