Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Dotyczy:SQL Server
Azure SQL Database
Azure SQL Managed Instance
Azure Synapse Analytics
Analytics Platform System (PDW)
SQL Database w Microsoft Fabric
Atak SQL injection to technika, która polega na wprowadzeniu złośliwego kodu SQL do łańcuchów znaków, które są następnie przesyłane do silnika bazy danych SQL Server w celu analizowania i wykonywania. Każda procedura tworząca zapytania SQL powinna zostać sprawdzona pod kątem luk w zabezpieczeniach wstrzykiwania SQL, ponieważ silnik bazy danych wykonuje wszystkie zapytania, które są składniowo poprawne. Nawet sparametryzowane dane mogą być manipulowane przez wykwalifikowanych i określonych atakujących.
Jak działa wstrzyknięcie kodu SQL
Podstawowa forma iniekcji SQL składa się z bezpośredniego wstawiania kodu do zmiennych wejściowych użytkownika, które są łączone z poleceniami SQL i wykonywane. Mniej bezpośredni atak wprowadza złośliwy kod do ciągów przeznaczonych do przechowywania w tabeli lub jako metadanych. Gdy przechowywane ciągi są następnie łączone w dynamicznym poleceniu SQL, jest wykonywany złośliwy kod.
Proces iniekcji działa przez przedwczesne zakończenie ciągu tekstowego i dołączenie nowego polecenia. Ponieważ wstawione polecenie może zawierać dodatkowe ciągi dołączone do niego przed jego wykonaniem, atakujący kończy wstrzykiwany ciąg znakiem komentarza --. Następujący tekst zostaje ignorowany w czasie wykonywania.
Poniższy skrypt przedstawia prostą iniekcję SQL. Skrypt tworzy zapytanie SQL, łącząc ciągi zakodowane na podstawie kodu wraz z ciągiem wprowadzonym przez użytkownika:
var ShipCity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";
Użytkownik zostanie poproszony o wprowadzenie nazwy miasta. Jeśli wprowadzają Redmondciąg , zapytanie zebrane przez skrypt wygląda podobnie do następującego przykładu:
SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';
Załóżmy jednak, że użytkownik wprowadza następujący tekst:
Redmond';drop table OrdersTable--
W takim przypadku skrypt tworzy następujące zapytanie:
SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'
Średnik (;) oznacza koniec jednego zapytania i początek drugiego. Podwójny łącznik (--) wskazuje, że pozostała część bieżącego wiersza jest komentarzem i powinna być ignorowana. Jeśli zmodyfikowany kod jest poprawny składniowo, jest wykonywany przez serwer. Gdy aparat bazy danych przetwarza to zapytanie, najpierw wybiera wszystkie rekordy w OrdersTable, gdzie ShipCity jest Redmond. Następnie silnik bazy danych usunie OrdersTable.
Tak długo, jak wstrzykiwany kod SQL jest poprawny syntatycznie, nie można wykryć manipulacji programowo. W związku z tym należy zweryfikować wszystkie dane wejściowe użytkownika i dokładnie przejrzeć kod, który wykonuje skonstruowane polecenia SQL na używanym serwerze. Najlepsze rozwiązania dotyczące kodowania opisano w poniższych sekcjach w tym artykule.
Weryfikowanie wszystkich danych wejściowych
Zawsze weryfikuj dane wejściowe użytkownika według typu testowania, długości, formatu i zakresu. Podczas implementowania środków ostrożności przed złośliwymi danymi wejściowymi należy wziąć pod uwagę scenariusze architektury i wdrażania aplikacji. Należy pamiętać, że programy przeznaczone do uruchamiania w bezpiecznym środowisku mogą być kopiowane do niezabezpieczonego środowiska. Poniższe sugestie powinny być uznawane za najlepsze rozwiązania:
Nie należy zakładać rozmiaru, typu ani zawartości danych odbieranych przez aplikację. Na przykład należy przeprowadzić następującą ocenę:
Jak działa aplikacja, jeśli błąd lub złośliwy użytkownik wprowadzi plik wideo o rozmiarze 2 GB, w którym aplikacja oczekuje kodu pocztowego?
Jak zachowuje się aplikacja, gdy instrukcja
DROP TABLEjest osadzona w polu tekstowym?
Przetestuj rozmiar i typ danych danych wejściowych i wymuś odpowiednie limity. Może to pomóc w zapobieganiu celowym przepełnieniu buforu.
Przetestuj zawartość zmiennych ciągu i zaakceptuj tylko oczekiwane wartości. Odrzuć wpisy zawierające dane binarne, sekwencje ucieczki i znaki komentarza. Może to pomóc zapobiec iniekcji skryptu i chronić przed niektórymi przepełnieniami buforu.
Podczas pracy z dokumentami XML zweryfikuj wszystkie dane względem jego schematu podczas ich wprowadzania.
Nigdy nie twórz instrukcji Transact-SQL bezpośrednio z danych wejściowych użytkownika.
Użyj procedur składowanych, aby zweryfikować dane wejściowe użytkownika.
W środowiskach wielowarstwowych wszystkie dane powinny zostać zweryfikowane przed wejściem do zaufanej strefy. Dane, które nie przechodzą przez proces weryfikacji, powinny zostać odrzucone, a błąd powinien zostać zwrócony do poprzedniej warstwy.
Zaimplementuj wiele warstw weryfikacji. Środki ostrożności wobec przypadkowych złośliwych użytkowników mogą być nieskuteczne wobec określonych osób atakujących. Lepszym rozwiązaniem jest zweryfikowanie danych wejściowych w interfejsie użytkownika i we wszystkich kolejnych punktach, w których przekracza granicę zaufania.
Na przykład weryfikacja danych w aplikacji po stronie klienta może uniemożliwić proste wstrzyknięcie skryptu. Jeśli jednak następna warstwa zakłada, że jego dane wejściowe są już weryfikowane, każdy złośliwy użytkownik, który może pominąć klienta, może mieć nieograniczony dostęp do systemu.
Nigdy nie należy łączyć danych wejściowych użytkownika, które nie są weryfikowane. Łączenie ciągów jest podstawowym punktem wejścia do iniekcji skryptu.
Nie akceptuj następujących ciągów w polach, z których można tworzyć nazwy plików:
AUX,CLOCK$,COM1doCOM8,CON,CONFIG$,LPT1doLPT8,NULiPRN.
Jeśli to możliwe, odrzuć dane wejściowe zawierające następujące znaki.
| Znak wejściowy | Znaczenie w Transact-SQL |
|---|---|
; |
Ogranicznik zapytania. |
' |
Ogranicznik ciągu danych znaków. |
-- |
Ogranicznik komentarza jednowierszowego. Tekst następujący -- do końca tego wiersza nie jest oceniany przez serwer. |
/*** ... ***/ |
Ograniczniki komentarzy. Tekst między elementami /* i */ nie jest oceniany przez serwer. |
xp_ |
Używany na początku nazwy składowanych w katalogu rozszerzonych procedur, takich jak xp_cmdshell. |
Używanie bezpiecznych parametrów SQL typu
Kolekcja Parameters w aparacie bazy danych zapewnia sprawdzanie typów i walidację długości. Jeśli używasz kolekcji Parameters , dane wejściowe są traktowane jako wartość literału zamiast jako kod wykonywalny. Kolejną zaletą korzystania z kolekcji Parameters jest możliwość wymuszania kontroli typu i długości. Wartości poza zakresem wyzwalają wyjątek. Poniższy fragment kodu przedstawia użycie kolekcji Parameters :
SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
SqlDbType.VarChar, 11);
parm.Value = Login.Text;
W tym przykładzie parametr @au_id jest traktowany jako wartość dosłowna zamiast jako kod wykonywalny. Ta wartość jest sprawdzana pod kątem typu i długości. Jeśli wartość @au_id nie jest zgodna z określonymi ograniczeniami dotyczącymi typu i długości, zgłaszany jest wyjątek.
Używanie sparametryzowanych danych wejściowych z procedurami składowanymi
Procedury składowane mogą być podatne na wstrzyknięcie kodu SQL, jeśli używają niefiltrowanych danych wejściowych. Na przykład następujący kod jest podatny na zagrożenia:
SqlDataAdapter myCommand =
new SqlDataAdapter("LoginStoredProcedure '" + Login.Text + "'", conn);
Jeśli używasz procedur składowanych, należy użyć parametrów jako danych wejściowych.
Używanie kolekcji Parameters z dynamicznym językiem SQL
Jeśli nie możesz używać procedur składowanych, nadal możesz użyć parametrów, jak pokazano w poniższym przykładzie kodu.
SqlDataAdapter myCommand = new SqlDataAdapter(
"SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn);
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
SqlDbType.VarChar, 11);
parm.Value = Login.Text;
Filtrowanie danych wejściowych
Filtrowanie danych wejściowych może być również pomocne w ochronie przed wstrzyknięciem kodu SQL przez usunięcie znaków ucieczki. Jednak ze względu na dużą liczbę znaków, które mogą stanowić problemy, filtrowanie nie jest niezawodną obroną. Poniższy przykład wyszukuje ogranicznik ciągu znaków.
private string SafeSqlLiteral(string inputSQL)
{
return inputSQL.Replace("'", "''");
}
Klauzule LIKE
Jeśli używasz klauzuli LIKE , symbole wieloznaczne nadal muszą zostać uniknięte:
s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");
Przegląd kodu pod kątem iniekcji SQL
Przejrzyj cały kod, który wywołuje EXECUTE, EXEC lub sp_executesql. Możesz użyć zapytań podobnych do poniższych, aby ułatwić identyfikowanie procedur zawierających te instrukcje. To zapytanie sprawdza 1, 2, 3 lub 4 spacje po wyrazach EXECUTE lub EXEC.
SELECT object_Name(id)
FROM syscomments
WHERE UPPER(TEXT) LIKE '%EXECUTE (%'
OR UPPER(TEXT) LIKE '%EXECUTE (%'
OR UPPER(TEXT) LIKE '%EXECUTE (%'
OR UPPER(TEXT) LIKE '%EXECUTE (%'
OR UPPER(TEXT) LIKE '%EXEC (%'
OR UPPER(TEXT) LIKE '%EXEC (%'
OR UPPER(TEXT) LIKE '%EXEC (%'
OR UPPER(TEXT) LIKE '%EXEC (%'
OR UPPER(TEXT) LIKE '%SP_EXECUTESQL%';
Zawijanie parametrów za pomocą funkcji QUOTENAME() i REPLACE()
W każdej wybranej procedurze składowanej sprawdź, czy wszystkie zmienne używane w dynamicznych Transact-SQL są prawidłowo obsługiwane. Dane pochodzące z parametrów wejściowych procedury składowanej lub odczytywane z tabeli powinny być opakowane w QUOTENAME() lub REPLACE(). Pamiętaj, że wartość @variable, która jest przekazywana QUOTENAME(), jest typu sysname i ma maksymalną długość 128 znaków.
| @variable | Zalecane opakowanie |
|---|---|
| Nazwa obiektu możliwego do zabezpieczenia | QUOTENAME(@variable) |
| Ciąg = <128 znaków | QUOTENAME(@variable, '''') |
| Ciąg z > 128 znakami | REPLACE(@variable,'''', '''''') |
W przypadku korzystania z tej techniki instrukcję SET można zmienić w następujący sposób:
-- Before:
SET @temp = N'SELECT * FROM authors WHERE au_lname ='''
+ @au_lname + N'''';
-- After:
SET @temp = N'SELECT * FROM authors WHERE au_lname = '''
+ REPLACE(@au_lname, '''', '''''') + N'''';
Iniekcja włączona przez obcinanie danych
Dowolna dynamiczna Transact-SQL przypisana zmiennej jest obcięta, jeśli jest większa niż bufor przydzielony tej zmiennej. Atakujący, który może wymusić obcięcie instrukcji, przekazując nieoczekiwanie długie ciągi do procedury składowanej, może manipulować końcowym wynikiem. Na przykład poniższa procedura składowania jest podatna na wstrzyknięcia, w trakcie których dochodzi do obcięcia.
W tym przykładzie mamy @command bufor o maksymalnej długości 200 znaków. Potrzebujemy łącznie 154 znaków, aby ustawić hasło 'sa': 26 dla UPDATE instrukcji, 16 dla WHERE klauzuli, 4 dla 'sa', i 2 dla znaków cudzysłowów otaczających QUOTENAME(@loginname): 200 - 26 - 16 - 4 - 2 = 154. Jednak ponieważ @new jest zadeklarowana jako nazwa sysname, ta zmienna może zawierać tylko 128 znaków. Możemy to przezwyciężyć, przekazując kilka pojedynczych cudzysłowów w pliku @new.
CREATE PROCEDURE sp_MySetPassword
@loginname SYSNAME,
@old SYSNAME,
@new SYSNAME
AS
-- Declare variable.
DECLARE @command VARCHAR(200)
-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password=' + QUOTENAME(@new, '''')
+ ' WHERE username=' + QUOTENAME(@loginname, '''')
+ ' AND password=' + QUOTENAME(@old, '''')
-- Execute the command.
EXEC (@command);
GO
Jeśli osoba atakująca przekaże 154 znaki do bufora 128 znaków, może ustawić nowe hasło sa bez znajomości starego hasła.
EXEC sp_MySetPassword 'sa',
'dummy',
'123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''
Z tego powodu należy użyć dużego buforu dla zmiennej polecenia lub bezpośrednio wykonać dynamiczne Transact-SQL wewnątrz EXECUTE instrukcji.
Obcinanie, gdy są używane funkcje QUOTENAME(@variable, '''''') oraz REPLACE()
Ciągi zwracane przez QUOTENAME() i REPLACE() są automatycznie obcinane, jeśli przekraczają przydzielone miejsce. Procedura przechowywana utworzona w poniższym przykładzie pokazuje, co może się wydarzyć.
W tym przykładzie dane przechowywane w zmiennych tymczasowych są obcinane, ponieważ rozmiar buforu @login@oldpassword, i @newpassword ma tylko 128 znaków, ale QUOTENAME() może zwrócić maksymalnie 258 znaków. Jeśli @new zawiera 128 znaków, @newpassword może to być 123... n, gdzie n jest 127 znakiem. Ponieważ ciąg zwracany przez QUOTENAME() jest obcięty, można go przekształcić tak, aby wyglądał podobnie do następującej instrukcji:
UPDATE Users SET password ='1234...[127] WHERE username=' -- other stuff here
CREATE PROCEDURE sp_MySetPassword
@loginname SYSNAME,
@old SYSNAME,
@new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);
SET @login = QUOTENAME(@loginname, '''');
SET @oldpassword = QUOTENAME(@old, '''');
SET @newpassword = QUOTENAME(@new, '''');
-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users set password = ' + @newpassword
+ ' WHERE username = ' + @login
+ ' AND password = ' + @oldpassword;
-- Execute the command.
EXEC (@command);
GO
W związku z tym poniższa instrukcja ustawia hasła wszystkich użytkowników na wartość przekazaną w poprzednim kodzie.
EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'
Obcinanie ciągów można wymusić przez przekroczenie przydzielonego miejsca buforu podczas korzystania z polecenia REPLACE(). Procedura przechowywana utworzona w poniższym przykładzie pokazuje, co może się wydarzyć.
W tym przykładzie dane są obcinane, ponieważ bufory przydzielone dla @login, @oldpassword i @newpassword mogą zawierać tylko 128 znaków, ale QUOTENAME() może zwracać maksymalnie 258 znaków. Jeśli @new zawiera 128 znaków, @newpassword może to być '123...n', gdzie n jest 127 znakiem. Ponieważ ciąg zwracany przez QUOTENAME() jest obcięty, można go przekształcić tak, aby wyglądał podobnie do następującej instrukcji:
UPDATE Users SET password='1234...[127] WHERE username=' -- other stuff here
CREATE PROCEDURE sp_MySetPassword
@loginname SYSNAME,
@old SYSNAME,
@new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);
SET @login = REPLACE(@loginname, '''', '''''');
SET @oldpassword = REPLACE(@old, '''', '''''');
SET @newpassword = REPLACE(@new, '''', '''''');
-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password = '''
+ @newpassword + ''' WHERE username = '''
+ @login + ''' AND password = ''' + @oldpassword + '''';
-- Execute the command.
EXEC (@command);
GO
Podobnie jak w przypadku metody QUOTENAME(), obcinania ciągów REPLACE() można uniknąć, deklarując zmienne tymczasowe, które są wystarczająco duże dla wszystkich przypadków. Jeśli to możliwe, wywołaj QUOTENAME() lub REPLACE() bezpośrednio wewnątrz dynamicznego języka Transact-SQL. W przeciwnym razie można obliczyć wymagany rozmiar buforu w następujący sposób. W przypadku @outbuffer = QUOTENAME(@input)parametru @outbuffer rozmiar powinien mieć wartość 2 * (len(@input) + 1). W przypadku użycia REPLACE() i podwojonych cudzysłowów, jak w poprzednim przykładzie, bufor o wielkości 2 * len(@input) jest wystarczający.
Następujące obliczenie obejmuje wszystkie przypadki:
WHILE LEN(@find_string) > 0, required buffer size =
ROUND(LEN(@input) / LEN(@find_string), 0)
* LEN(@new_string) + (LEN(@input) % LEN(@find_string))
Obcięcie, gdy jest używana funkcja QUOTENAME(@variable, ']')
Obcinanie może wystąpić, gdy nazwa obiektu zabezpieczanego przez silnik bazy danych jest przekazywana do instrukcji używających formy QUOTENAME(@variable, ']'). W poniższym przykładzie pokazano ten scenariusz.
W tym przykładzie @objectname musi umożliwiać 2 * 258 + 1 znaków.
CREATE PROCEDURE sp_MyProc
@schemaname SYSNAME,
@tablename SYSNAME
AS
-- Declare a variable as sysname. The variable will be 128 characters.
DECLARE @objectname SYSNAME;
SET @objectname = QUOTENAME(@schemaname) + '.' + QUOTENAME(@tablename);
-- Do some operations.
GO
Podczas łączenia wartości typu sysname należy użyć zmiennych tymczasowych wystarczająco dużych, aby przechowywać maksymalnie 128 znaków na wartość. Jeśli to możliwe, wywołaj QUOTENAME() bezpośrednio w dynamicznym języku Transact-SQL. W przeciwnym razie można obliczyć wymagany rozmiar buforu, jak wyjaśniono w poprzedniej sekcji.