SQL injection

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:

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.

Risorse