Categoria OWASP: MASVS-CODE: Qualità del codice
Panoramica
L'SQL injection sfrutta le applicazioni vulnerabili inserendo codice nelle istruzioni SQL per accedere ai database sottostanti oltre le loro interfacce esposte intenzionalmente. L'attacco può esporre dati privati, danneggiare i contenuti del database e persino compromettere l'infrastruttura di backend.
SQL può essere vulnerabile all'inserimento tramite query create dinamicamente concatenando l'input dell'utente prima dell'esecuzione. Sfruttando le applicazioni web, mobile e qualsiasi database SQL, l'SQL injection di solito figura nella Top Ten di OWASP delle vulnerabilità web. Gli aggressori hanno utilizzato la tecnica in diverse violazioni di alto profilo.
In questo esempio di base, un input non sfuggito da un utente in una casella del numero d'ordine può essere inserito nella stringa SQL e interpretato come la seguente query:
SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1
Questo codice genererebbe un errore di sintassi del database in una console web, a indicare che l'applicazione potrebbe essere vulnerabile a SQL injection. La sostituzione del numero d'ordine con 'OR 1=1–
significa che l'autenticazione può essere eseguita poiché il database valuta l'istruzione come True
, poiché uno è sempre uguale a uno.
Analogamente, questa query restituisce tutte le righe di una tabella:
SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;
Fornitori di contenuti
I fornitori di contenuti offrono un meccanismo di archiviazione strutturato che può essere limitato a un'applicazione o esportato per la condivisione con altre app. Le autorizzazioni devono essere impostate in base al principio del privilegio minimo. Un ContentProvider
esportato può avere una singola autorizzazione specificata per la lettura e la scrittura.
È importante notare che non tutte le iniezioni SQL portano a uno sfruttamento. Alcuni fornitori di contenuti già concedono ai lettori l'accesso completo al database SQLite; la possibilità di eseguire query arbitrarie offre scarsi vantaggi. I pattern che possono rappresentare un problema di sicurezza includono:
- Più fornitori di contenuti che condividono un unico file del database SQLite.
- In questo caso, ogni tabella potrebbe essere destinata a un unico fornitore di contenuti. Un'iniezione SQL riuscita in un content provider concede l'accesso a qualsiasi altra tabella.
- Un fornitore di contenuti dispone di più autorizzazioni per i contenuti all'interno dello stesso database.
- L'iniezione SQL in un singolo fornitore di contenuti che concede l'accesso con livelli di autorizzazione diversi potrebbe comportare il bypass locale delle impostazioni di sicurezza o della privacy.
Impatto
L'SQL injection può esporre dati sensibili di utenti o applicazioni, aggirare le restrizioni di autenticazione e autorizzazione e lasciare i database vulnerabili a danneggiamenti o eliminazioni. Gli impatti possono includere conseguenze pericolose e durature per gli utenti i cui dati personali sono stati esposti. I fornitori di app e servizi rischiano di perdere la proprietà intellettuale o la fiducia degli utenti.
Mitigazioni
Parametri sostituibili
L'utilizzo di ?
come parametro sostituibile nelle clausole di selezione e di un array distinto di argomenti di selezione lega l'input utente direttamente alla query anziché interpretarlo come parte di un'istruzione SQL.
Kotlin
// Constructs a selection clause with a replaceable parameter.
val selectionClause = "var = ?"
// Sets up an array of arguments.
val selectionArgs: Array<String> = arrayOf("")
// Adds values to the selection arguments array.
selectionArgs[0] = userInput
Java
// Constructs a selection clause with a replaceable parameter.
String selectionClause = "var = ?";
// Sets up an array of arguments.
String[] selectionArgs = {""};
// Adds values to the selection arguments array.
selectionArgs[0] = userInput;
L'input dell'utente viene associato direttamente alla query anziché essere trattato come SQL, impedendo l'inserimento di codice.
Ecco un esempio più elaborato che mostra la query di un'app di shopping per recuperare i dettagli degli acquisti con parametri sostituibili:
Kotlin
fun validateOrderDetails(email: String, orderNumber: String): Boolean {
val cursor = db.rawQuery(
"select * from purchases where EMAIL = ? and ORDER_NUMBER = ?",
arrayOf(email, orderNumber)
)
val bool = cursor?.moveToFirst() ?: false
cursor?.close()
return bool
}
Java
public boolean validateOrderDetails(String email, String orderNumber) {
boolean bool = false;
Cursor cursor = db.rawQuery(
"select * from purchases where EMAIL = ? and ORDER_NUMBER = ?",
new String[]{email, orderNumber});
if (cursor != null) {
if (cursor.moveToFirst()) {
bool = true;
}
cursor.close();
}
return bool;
}
Utilizzare gli oggetti PreparedStatement
L'interfaccia PreparedStatement
precompila le istruzioni SQL come un oggetto che può essere eseguito in modo efficiente più volte. PreparedStatement utilizza ?
come segnaposto per i parametri, il che rende inefficace il seguente tentativo di inserimento compilato:
WHERE id=295094 OR 1=1;
In questo caso, l'istruzione 295094 OR 1=1
viene letta come valore per l'ID, probabilmente senza generare risultati, mentre una query non elaborata interpreterebbe l'istruzione OR 1=1
come un'altra parte della clausola WHERE
.
L'esempio seguente mostra una query con parametri:
Kotlin
val pstmt: PreparedStatement = con.prepareStatement(
"UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?").apply {
setString(1, "Barista")
setInt(2, 295094)
}
Java
PreparedStatement pstmt = con.prepareStatement(
"UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?");
pstmt.setString(1, "Barista")
pstmt.setInt(2, 295094)
Utilizzare i metodi di query
In questo esempio più lungo, selection
e selectionArgs
del metodo query()
vengono combinati per creare una clausola WHERE
. Poiché gli argomenti vengono forniti separatamente, vengono sottoposti a evocazione prima della loro combinazione, impedendo l'attacco SQL injection.
Kotlin
val db: SQLiteDatabase = dbHelper.getReadableDatabase()
// Defines a projection that specifies which columns from the database
// should be selected.
val projection = arrayOf(
BaseColumns._ID,
FeedEntry.COLUMN_NAME_TITLE,
FeedEntry.COLUMN_NAME_SUBTITLE
)
// Filters results WHERE "title" = 'My Title'.
val selection: String = FeedEntry.COLUMN_NAME_TITLE.toString() + " = ?"
val selectionArgs = arrayOf("My Title")
// Specifies how to sort the results in the returned Cursor object.
val sortOrder: String = FeedEntry.COLUMN_NAME_SUBTITLE.toString() + " DESC"
val cursor = db.query(
FeedEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return
// (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // Don't group the rows
null, // Don't filter by row groups
sortOrder // The sort order
).use {
// Perform operations on the query result here.
it.moveToFirst()
}
Java
SQLiteDatabase db = dbHelper.getReadableDatabase();
// Defines a projection that specifies which columns from the database
// should be selected.
String[] projection = {
BaseColumns._ID,
FeedEntry.COLUMN_NAME_TITLE,
FeedEntry.COLUMN_NAME_SUBTITLE
};
// Filters results WHERE "title" = 'My Title'.
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };
// Specifies how to sort the results in the returned Cursor object.
String sortOrder =
FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";
Cursor cursor = db.query(
FeedEntry.TABLE_NAME, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
sortOrder // The sort order
);
Utilizza SQLiteQueryBuilder configurato correttamente
Gli sviluppatori possono proteggere ulteriormente le applicazioni utilizzando SQLiteQueryBuilder
, una classe che consente di creare query da inviare agli oggetti SQLiteDatabase
. Le configurazioni consigliate includono:
- Modalità
setStrict()
per la convalida delle query. setStrictColumns()
per verificare che le colonne siano incluse nella lista consentita in setProjectionMap.setStrictGrammar()
per limitare le sottoquery.
Utilizzare la libreria Room
Il pacchetto android.database.sqlite
fornisce le API necessarie per l'utilizzo dei database su Android. Tuttavia, questo approccio richiede la scrittura di codice a basso livello e non dispone della verifica in fase di compilazione delle query SQL non elaborate. Quando i grafici dei dati cambiano, le query SQL interessate devono essere aggiornate manualmente, un processo che richiede tempo e può generare errori.
Una soluzione di alto livello consiste nell'utilizzare la Room Persistence Library come livello di astrazione per i database SQLite. Le caratteristiche della camera includono:
- Una classe di database che funge da punto di accesso principale per la connessione ai dati permanenti dell'app.
- Entità di dati che rappresentano le tabelle del database.
- Oggetti di accesso ai dati (DAO), che forniscono metodi che l'app può utilizzare per eseguire query, aggiornare, inserire ed eliminare i dati.
I vantaggi della Sala includono:
- Verifica in fase di compilazione delle query SQL.
- Riduzione del codice boilerplate soggetto a errori.
- Migrazione del database semplificata.
Best practice
L'SQL injection è un attacco potente contro cui può essere difficile essere completamente resilienti, in particolare con applicazioni grandi e complesse. È necessario adottare ulteriori considerazioni di sicurezza per limitare la gravità di potenziali difetti nelle interfacce di dati, tra cui:
- Hash robusti, unidirezionali e con salt per criptare le password:
- AES a 256 bit per applicazioni commerciali.
- Dimensioni delle chiavi pubbliche di 224 o 256 bit per la crittografia ellittica.
- Limitare le autorizzazioni.
- Strutturare con precisione i formati dei dati e verificare che i dati siano conformi al formato previsto.
- Evitare di memorizzare dati utente personali o sensibili, se possibile (ad esempio, implementando la logica dell'applicazione tramite hashing anziché trasmettere o memorizzare i dati).
- Riduci al minimo le API e le applicazioni di terze parti che accedono a dati sensibili.