Categoria OWASP: MASVS-CODE: Code Quality
Panoramica
SQL injection sfrutta le applicazioni vulnerabili inserendo codice nelle istruzioni SQL per accedere ai database sottostanti oltre le interfacce esposte intenzionalmente. L'attacco può esporre dati privati, compromettere i contenuti del database e persino compromettere l'infrastruttura di backend.
SQL può essere vulnerabile all'injection tramite query create dinamicamente concatenando l'input dell'utente prima dell'esecuzione. L'SQL injection, che prende di mira le applicazioni web, mobile e di database SQL, di solito è presente nella OWASP Top Ten delle vulnerabilità web. Gli aggressori hanno utilizzato questa tecnica in diverse violazioni di alto profilo.
In questo esempio di base, un input non sottoposto a escape da parte di un utente in una casella del numero dell'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, il che indica che l'applicazione potrebbe essere vulnerabile all'SQL injection. La sostituzione del numero dell'ordine con 'OR 1=1– consente di ottenere l'autenticazione, poiché il database valuta l'istruzione come True, in quanto uno è sempre uguale a uno.
Allo stesso modo, 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 SQL injection portano allo sfruttamento. Alcuni fornitori di contenuti concedono già ai lettori l'accesso completo al database SQLite; la possibilità di eseguire query arbitrarie offre pochi vantaggi. I pattern che possono rappresentare un problema di sicurezza includono:
- Più fornitori di contenuti che condividono un singolo file di database SQLite.
- In questo caso, ogni tabella potrebbe essere destinata a un fornitore di contenuti univoco. Un'SQL injection riuscita in un fornitore di contenuti concederebbe l'accesso a qualsiasi altra tabella.
- Un fornitore di contenuti ha più autorizzazioni per i contenuti all'interno dello stesso database.
- L'SQL injection in un singolo fornitore di contenuti che concede l'accesso con diversi livelli di autorizzazione potrebbe portare a un bypass locale delle impostazioni di sicurezza o privacy.
Impatto
L'SQL injection può esporre dati sensibili dell'utente o dell'applicazione, superare le restrizioni di autenticazione e autorizzazione e lasciare i database vulnerabili alla compromissione o all'eliminazione. Gli impatti possono includere implicazioni 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 separato di argomenti di selezione associa l'input dell'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 è associato direttamente alla query anziché essere trattato come SQL, impedendo l'injection di codice.
Ecco un esempio più elaborato che mostra la query di un'app di shopping per recuperare i dettagli dell'acquisto 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 oggetti PreparedStatement
L'interfaccia PreparedStatement precompila le istruzioni SQL come oggetto che può essere eseguito in modo efficiente più volte. `PreparedStatement` utilizza ? come segnaposto per i parametri, il che renderebbe inefficace il seguente tentativo di injection compilato:
WHERE id=295094 OR 1=1;
In questo caso, l'istruzione 295094 OR 1=1 viene letta come il valore dell'ID, probabilmente senza produrre 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 escape prima della combinazione, impedendo l'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
);
Utilizzare SQLiteQueryBuilder configurato correttamente
Gli sviluppatori possono proteggere ulteriormente le applicazioni utilizzando SQLiteQueryBuilder, una classe che aiuta a creare query da inviare agli oggetti SQLiteDatabase. Le configurazioni consigliate includono:
setStrict()modalità per la convalida delle query.setStrictColumns()per convalidare che le colonne siano incluse nell'elenco consentito 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 di basso livello e non prevede la verifica in fase di compilazione delle query SQL non elaborate. Man mano che i grafici dei dati cambiano, le query SQL interessate devono essere aggiornate manualmente, un processo dispendioso in termini di tempo e soggetto a errori.
Una soluzione di alto livello consiste nell'utilizzare la libreria di persistenza Room come livello di astrazione per i database SQLite. Le funzionalità di Room comprendono:
- Una classe di database che funge da punto di accesso principale per la connessione ai dati persistenti 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 dati.
I vantaggi di Room includono:
- Verifica in fase di compilazione delle query SQL.
- Riduzione del codice boilerplate soggetto a errori.
- Migrazione semplificata del database.
Best practice
L'SQL injection è un attacco potente contro il quale può essere difficile essere completamente resilienti, in particolare con applicazioni di grandi dimensioni e complesse. Devono essere in vigore ulteriori considerazioni sulla sicurezza per limitare la gravità di potenziali difetti nelle interfacce dei dati, tra cui:
- Hash robusti, unidirezionali e con salt per criptare le password:
- AES a 256 bit per le applicazioni commerciali.
- Dimensioni delle chiavi pubbliche di 224 o 256 bit per la crittografia a curva ellittica.
- Limitazione delle autorizzazioni.
- Strutturazione precisa dei formati dei dati e verifica che i dati siano conformi al formato previsto.
- Evitare di archiviare dati personali o sensibili dell'utente, ove possibile (ad esempio, implementare la logica dell'applicazione tramite hashing anziché trasmettere o archiviare i dati).
- Riduzione al minimo delle API e delle applicazioni di terze parti che accedono a dati sensibili.