Czym jest SQL injection, jak wygląda anatomia ataku oraz jak zabezpieczyć kod aplikacji? Odpowiedzi na zadane pytania wraz z garścią dodatkowych informacji w krótkim wpisie.
Nie skłamię, twierdząc, że niemal każda współczesna aplikacja korzysta z baz danych, przez co pojawia się konieczność zapewnienia elastycznego dostępu. Niestety zdarza się, że osoby odpowiedzialne za przygotowanie warstwy dostępu do danych zapominają lub najzwyczajniej nie zdają sobie sprawy z zagrożeń związanych z niewłaściwym zabezpieczeniem owych mechanizmów.
private static DataTable GetAuthors(string firstname)
{
using (var conn = new SqlConnection(connectionString))
{
var data = new DataTable();
conn.Open();
string query = $"SELECT * FROM Author WHERE Ath_Firstname = '{firstname}'";
using (var cmd = new SqlCommand(query, conn))
{
data.Load(cmd.ExecuteReader());
return data;
}
}
}
Na przykładzie powyższej metody, której zadaniem jest dostarczenie informacji o autorach spełniających warunek, przeanalizujemy zagadnienie. W parametrze przekazujemy imię autora, które trafia do warunku zapytania. Wartość wprowadzana jest wprost do ciągu znaków reprezentującego kwerendę. Osoba implementująca oczekuje przekazania imienia autora, natomiast napastnik będzie testował metodę na podatności. Wprowadzenie pojedynczego apostrofa spowoduje przedwczesne zakończenie zapytania, umożliwiając pozyskanie kontroli. W sytuacji, gdy interfejs wykorzystywany jest za pośrednictwem niezaufanych klientów, np. przeglądarkę www, szczególną uwagę należy zwrócić na zapewnienie bezpieczeństwa.
Biblioteki odpowiedzialne za dostęp do bazy danych najczęściej implementują mechanizmy zabezpieczające wartości przekazywane przez niezaufane źródła, poprzez zastosowanie kwerend parametrycznych. Mechanizmy te oczyszczają przekazywane wartości. Poniżej przedstawiam metodę z wykorzystaniem parametrów. Warto zadbać również o zakres zwracanych danych, zamiana *
nie tylko zmniejszy zakres pozyskanych informacji podczas skanowania, ale pozwoli na wydajniejszą pracę silnika bazy danych.
private static DataTable SafeGetAuthors(string firstname)
{
using (var conn = new SqlConnection(connectionString))
{
var data = new DataTable();
conn.Open();
string query = $"SELECT Ath_ID, Ath_Surname, Ath_FirstName
FROM Author
WHERE Ath_FirstName = @FirstName";
using (var cmd = new SqlCommand(query, conn))
{
cmd.Parameters.Add(new SqlParameter("@FirstName", firstname));
data.Load(cmd.ExecuteReader());
return data;
}
}
}
Poniżej udostępniam kod umożliwiający przetestowanie podatności. Zachęcam do połączenia z bazą demonstracyjną i wykonania testów penetracyjnych.
using System;
using System.Data;
using System.Data.SqlClient;
namespace SQLInjection
{
class Program
{
private static string connectionString
{
get
{
return "server=Demo;user id=j@ro;password=Prezes!;initial catalog=***** ***";
}
}
static void Main(string[] args)
{
Console.WriteLine("Insert a value");
var authors = GetAuthors("Robert");
Console.WriteLine("firstname=Robert");
Console.WriteLine($"Count: {authors.Rows.Count}");
var authorsSQLInjection = GetAuthors("Robert' OR 1 = 1 --");
Console.WriteLine("firstname=Robert' OR 1 = 1 --");
Console.WriteLine($"Count: {authorsSQLInjection.Rows.Count}");
Console.WriteLine("--------------------------------------");
Console.WriteLine("Used parameters");
authors = SafeGetAuthors("Robert");
Console.WriteLine("firstname=Robert");
Console.WriteLine($"Count: {authors.Rows.Count}");
authorsSQLInjection = SafeGetAuthors("Robert' OR 1 = 1 --");
Console.WriteLine("firstname=Robert' OR 1 = 1 --");
Console.WriteLine($"Count: {authorsSQLInjection.Rows.Count}");
}
private static DataTable GetAuthors(string firstname)
{
using (var conn = new SqlConnection(connectionString))
{
var data = new DataTable();
conn.Open();
string query = $"SELECT * FROM Author WHERE Ath_FirstName = '{firstname}'";
using (var cmd = new SqlCommand(query, conn))
{
data.Load(cmd.ExecuteReader());
return data;
}
}
}
private static DataTable SafeGetAuthors(string firstname)
{
using (var conn = new SqlConnection(connectionString))
{
var data = new DataTable();
conn.Open();
string query = $"SELECT Ath_ID, Ath_Surname, Ath_FirstName
FROM Author
WHERE Ath_FirstName = @FirstName";
using (var cmd = new SqlCommand(query, conn))
{
cmd.Parameters.Add(new SqlParameter("@FirstName", firstname));
data.Load(cmd.ExecuteReader());
return data;
}
}
}
}
}
Insert a value
firstname=Robert
Count: 1
firstname=Robert' OR 1 = 1 --
Count: 2
--------------------------------------
Used parameters
firstname=Robert
Count: 1
firstname=Robert' OR 1 = 1 --
Count: 0
Na ataki typu SQL injection najbardziej narażone są aplikacje webowe udostępnione publicznie, które można "testować" poprzez przeprowadzenie fuzzingu parametrów dostarczanych przez HTTP. Oprócz użycia mechanizmów zabezpieczających kwerendy warto zapewnić niski poziom uprawnień dla połączeń wykorzystywanych przez niezaufane źródła. W sytuacji, gdy pojawi się problem wynikający z braku uprawnień, administrator w każdej chwili ma możliwość ich nadania. Każda taka sytuacja dodatkowo pozwala na weryfikację, czy aplikacja na pewno powinna posiadać brakujące uprawnienia.
Zwracajmy uwagę na sposób budowy kwerend, każda publiczna usługa może zostać przeskanowana przez osoby trzecie w celu pozyskania informacji. Miejmy na uwadze, że nadanie niskich uprawnień oraz ich odpowiednie konfigurowanie jest znacznie mniej kosztowne niżeli utrata kontroli nad bazą danych.
Troska Robert