wstrzyknięcie kodu SQL

Kategoria OWASP: MASVS-CODE: Code Quality

Omówienie

Wstrzyknięcie kodu SQL wykorzystuje podatne aplikacje, wstawiając kod do instrukcji SQL, aby uzyskać dostęp do baz danych poza ich celowo udostępnionymi interfejsami. Atak może spowodować ujawnienie danych prywatnych, uszkodzenie zawartości bazy danych, a nawet skompromitowanie infrastruktury zaplecza.

Kod SQL może być podatny na wstrzykiwanie za pomocą zapytań, które są tworzone dynamicznie przez złączenie danych wejściowych użytkownika przed wykonaniem. Wstrzyknięcie kodu SQL jest zwykle stosowane do atakowania witryn, aplikacji mobilnych i dowolnych aplikacji bazy danych SQL. Jest to jedna z 10 najczęstszych zagrożeń OWASP związanych z lukami w zabezpieczeniach witryn. Osoby przeprowadzające ataki korzystały z tej techniki w przypadku kilku znanych przypadków naruszenia bezpieczeństwa.

W tym podstawowym przykładzie nieescapaowany ciąg znaków wprowadzony przez użytkownika w polu numeru zamówienia może zostać wstawiony do ciągu znaków SQL i zinterpretowany jako następujące zapytanie:

SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1

Taki kod wygeneruje błąd składni bazy danych w konsoli internetowej, co wskazuje, że aplikacja może być podatna na wstrzyknięcie kodu SQL. Zastąpienie numeru zamówienia wartością 'OR 1=1– oznacza, że można przeprowadzić uwierzytelnianie, ponieważ baza danych ocenia to twierdzenie jako True, ponieważ 1 zawsze równa się 1.

Podobnie to zapytanie zwraca wszystkie wiersze z tabeli:

SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;

Dostawcy treści

Dostawcy treści oferują uporządkowany mechanizm przechowywania, który może być ograniczony do aplikacji lub wyeksportowany w celu udostępnienia innym aplikacjom. Uprawnienia powinny być ustawiane zgodnie z zasadą jak najmniejszych uprawnień. Wyeksportowany ContentProvider może mieć jedno określone uprawnienie do odczytu i zapisu.

Warto pamiętać, że nie wszystkie iniekcje SQL prowadzą do wykorzystania podatności. Niektórzy dostawcy treści już teraz przyznają czytelnikom pełny dostęp do bazy danych SQLite. Możliwość wykonywania dowolnych zapytań nie daje zbyt wielu korzyści. Do wzorców, które mogą stanowić problem z bezpieczeństwem, należą:

  • Kilku dostawców treści udostępniających jeden plik bazy danych SQLite.
    • W takim przypadku każda tabela może być przeznaczona dla konkretnego dostawcy treści. Udało się wstrzyknąć kod SQL do jednego komponentu ContentProvider, co dało dostęp do wszystkich innych tabel.
  • Dostawca treści ma wiele uprawnień do treści w tej samej bazie danych.
    • Wstrzyknięcie kodu SQL w jednym dostawcy treści, który przyznaje dostęp z różnymi poziomami uprawnień, może spowodować lokalne obejście ustawień zabezpieczeń lub prywatności.

Wpływ

Wstrzyknięcie kodu SQL może ujawnić poufne dane użytkownika lub aplikacji, obejść ograniczenia uwierzytelniania i autoryzacji oraz narazić bazy danych na uszkodzenie lub usunięcie. Skutki mogą obejmować niebezpieczne i trwałe konsekwencje dla użytkowników, których dane osobowe zostały ujawnione. Dostawcy aplikacji i usług ryzykują utratę własności intelektualnej lub zaufania użytkowników.

Środki zaradcze

Parametry zastępcze

Użycie wartości ? jako parametru wymiennego w klauzulach wyboru i w odrębnej tablicy argumentów wyboru powoduje bezpośrednie powiązanie danych wejściowych użytkownika z zapytaniem zamiast ich interpretowania jako części instrukcji 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;

Dane wejściowe użytkownika są powiązane bezpośrednio z zapytaniem, a nie traktowane jako kod SQL, co zapobiega wstrzykiwaniu kodu.

Oto bardziej szczegółowy przykład zapytania aplikacji zakupowej służącego do pobierania szczegółów zakupu z parametrami, które można zastąpić:

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;
}

Używanie obiektów PreparedStatement

Interfejs PreparedStatement wstępnie kompiluje instrukcje SQL jako obiekt, który można następnie wydajnie wykonywać wielokrotnie. Funkcja PreparedStatement używa znaku ? jako zastępnika parametrów, co uniemożliwi skuteczność następującej próby wstrzyknięcia skompilowanego kodu:

WHERE id=295094 OR 1=1;

W tym przypadku instrukcja 295094 OR 1=1 jest interpretowana jako wartość identyfikatora, co prawdopodobnie nie spowoduje uzyskania żadnych wyników, podczas gdy zapytanie w postaci tekstu niesformatowanego zinterpretuje instrukcję OR 1=1 jako część wyrażenia WHERE. Przykład poniżej pokazuje zapytanie z parametrami:

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)

Używanie metod zapytań

W tym dłuższym przykładzie elementy selectionselectionArgs metody query() są łączone, aby utworzyć klauzulę WHERE. Argumenty są podawane osobno, więc przed ich połączeniem są ujęte w znaki ucieczki, co zapobiega wstrzyknięciu kodu SQL.

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
    );

Użyj prawidłowo skonfigurowanego obiektu SQLiteQueryBuilder.

Deweloperzy mogą dodatkowo chronić aplikacje, korzystając z klasy SQLiteQueryBuilder, która pomaga tworzyć zapytania do wysłania do obiektów SQLiteDatabase. Zalecane konfiguracje:

Korzystanie z biblioteki Room

Pakiet android.database.sqlite udostępnia interfejsy API potrzebne do korzystania z baz danych na Androidzie. Takie podejście wymaga jednak napisania kodu niskiego poziomu i nie zapewnia weryfikacji nieprzetworzonych zapytań SQL w czasie kompilacji. Gdy zmieniają się wykresy danych, należy ręcznie zaktualizować zapytania SQL, które ich dotyczą. Jest to czasochłonny i podatny na błędy proces.

Rozwiązaniem ogólnym jest użycie biblioteki trwałości Room jako warstwy abstrakcji dla baz danych SQLite. Funkcje pokoju:

  • Klasa bazy danych, która służy jako główny punkt dostępu do trwałych danych aplikacji.
  • Elementy danych reprezentujące tabele bazy danych.
  • obiekty dostępu do danych (DAO), które udostępniają metody, których aplikacja może używać do wysyłania zapytań, aktualizowania, wstawiania i usuwania danych;

Zalety pokoju:

  • weryfikacja zapytań SQL w czasie kompilacji;
  • Zmniejszenie liczby powtarzalnego kodu podatnego na błędy.
  • usprawnić migrację bazy danych.

Sprawdzone metody

Wstrzyknięcie kodu SQL to skuteczny atak, przed którym trudno się w pełni zabezpieczyć, zwłaszcza w przypadku dużych i złożonych aplikacji. Należy wziąć pod uwagę dodatkowe kwestie bezpieczeństwa, aby ograniczyć wagę potencjalnych błędów w interfejsach danych, w tym:

  • Sprawdzone, jednokierunkowe i zawierające sól skróty do szyfrowania haseł:
    • 256-bitowy AES do zastosowań komercyjnych.
    • Klucze publiczne o rozmiarze 224 lub 256 bitów w przypadku kryptografii krzywych eliptycznych.
  • Ograniczanie uprawnień.
  • Dokładne uporządkowanie formatów danych i sprawdzenie, czy dane są zgodne z oczekiwanym formatem.
  • unikanie przechowywania danych osobowych lub poufnych użytkowników (np. przez implementowanie logiki aplikacji za pomocą funkcji szyfrowania z kluczem haszującym zamiast przesyłania lub przechowywania danych);
  • ograniczanie liczby interfejsów API i aplikacji innych firm, które mają dostęp do danych wrażliwych;

Materiały