Sdílet prostřednictvím


Injektáž SQL

platí pro:SQL ServerAzure SQL DatabaseAzure SQL Managed InstanceAzure Synapse AnalyticsAnalytics Platform System (PDW)databáze SQL v Microsoft Fabric

Injektáž SQL je útok, ve kterém se škodlivý kód vloží do řetězců, které se později předávají instanci databázového stroje SQL Serveru pro analýzu a spuštění. Všechny procedury, které vytváří příkazy SQL, by měly být kontrolovány na zranitelnosti injekcí, protože databázový stroj spouští všechny syntakticky platné dotazy, které přijímá. I parametrizovaná data mohou být manipulována zkušeným a určeným útočníkem.

Jak funguje injektáž SQL

Primární forma injektáže SQL se skládá z přímého vložení kódu do proměnných uživatelského vstupu, které jsou zřetězeny příkazy SQL a spuštěny. Méně přímý útok vloží škodlivý kód do řetězců určených pro úložiště v tabulce nebo jako metadata. Když se uložené řetězce zřetědí do dynamického příkazu SQL, spustí se škodlivý kód.

Proces injektáže funguje předčasně ukončením textového řetězce a připojením nového příkazu. Vzhledem k tomu, že vložený příkaz může mít před spuštěním k němu připojené další řetězce, ukončí útočník vložený řetězec symbolem komentáře --. Následující text se v době provádění ignoruje.

Následující skript ukazuje jednoduchou injektáž SQL. Skript sestaví dotaz SQL zřetězením pevně zakódovaných řetězců společně s řetězcem zadaným uživatelem:

var ShipCity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

Uživateli se zobrazí výzva k zadání názvu města. Pokud zadá Redmond, dotaz sestavený skriptem vypadá podobně jako v následujícím příkladu:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

Předpokládejme však, že uživatel zadá následující text:

Redmond';drop table OrdersTable--

V tomto případě skript sestaví následující dotaz:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'

Středník (;) označuje konec jednoho dotazu a začátek druhého dotazu. Dvojitá pomlčka (--) označuje, že zbytek aktuálního řádku je komentář a měl by být ignorován. Pokud je upravený kód syntakticky správný, provede ho server. Když databázový stroj tento příkaz zpracuje, nejprve vybere všechny záznamy v místě, kde OrdersTableShipCity je Redmond. Poté databázový stroj zruší OrdersTable.

Pokud je vložený kód SQL syntakticky správný, manipulace se nedá detekovat programově. Proto musíte ověřit veškerý uživatelský vstup a pečlivě zkontrolovat kód, který provádí vytvořené příkazy SQL na serveru, který používáte. Osvědčené postupy kódování jsou popsány v následujících částech tohoto článku.

Ověření veškerého vstupu

Vždy ověřte vstup uživatele podle typu testování, délky, formátu a rozsahu. Při implementaci bezpečnostních opatření proti škodlivému vstupu zvažte scénáře architektury a nasazení vaší aplikace. Mějte na paměti, že programy navržené tak, aby běžely v zabezpečeném prostředí, lze kopírovat do nezabezpečeného prostředí. Doporučované návrhy by měly být zváženy jako osvědčené postupy:

  • Nepředpokládáme velikost, typ nebo obsah dat přijatých vaší aplikací. Měli byste například provést následující vyhodnocení:

    • Jak se vaše aplikace chová, pokud uživatel se zlými úmysly zadá soubor videa o velikosti 2 GB, kde vaše aplikace očekává PSČ?

    • Jak se vaše aplikace chová, pokud je příkaz DROP TABLE vložen do textového pole?

  • Otestujte velikost a datový typ vstupu a vynucujte odpovídající limity. To může pomoct zabránit záměrnému přetečení vyrovnávací paměti.

  • Otestujte obsah řetězcových proměnných a přijměte pouze očekávané hodnoty. Odmítne položky obsahující binární data, řídicí sekvence a znaky komentářů. To může pomoct zabránit injektáži skriptu a chránit před některými zneužitími přetečení vyrovnávací paměti.

  • Při práci s dokumenty XML ověřte všechna data ve schématu při jejich zadávání.

  • Nikdy nevystavujte Transact-SQL příkazy přímo ze vstupu uživatele.

  • K ověření uživatelského vstupu použijte uložené procedury.

  • Ve vícevrstvém prostředí by se všechna data měla ověřit před vstupem do důvěryhodné zóny. Data, která neprojdou procesem ověření, by se měla zamítnout a měla by se vrátit chyba na předchozí vrstvě.

  • Implementujte více vrstev ověřování. Preventivní opatření proti příležitostným uživatelům se zlými úmysly mohou být neefektivní vůči určeným útočníkům. Lepším postupem je ověřit vstup v uživatelském rozhraní a ve všech následných bodech, kde překračuje hranici důvěryhodnosti.

    Ověření dat v aplikaci na straně klienta může například zabránit injektáži jednoduchého skriptu. Pokud ale další úroveň předpokládá, že jeho vstup je již ověřen, může mít každý škodlivý uživatel, který může obejít klienta, neomezený přístup k systému.

  • Nikdy nespojujte uživatelský vstup, který není ověřený. Zřetězení řetězců je primárním vstupním bodem pro injekci skriptu.

  • Nepřijme následující řetězce v polích, ze kterých lze vytvořit názvy souborů: AUX, CLOCK$přes COM1COM8, CON, , CONFIG$, až , LPT1, LPT8NULa PRN.

Pokud je to možné, zamítněte vstup, který obsahuje následující znaky.

Vstupní znak Význam v Transact-SQL
; Oddělovač dotazů
' Oddělovač datových řetězců znaků
-- Jednořádkový oddělovač komentářů Text následující -- až do konce tohoto řádku není serverem vyhodnocen.
/*** ... ***/ Oddělovače komentářů Text mezi /* a */ není serverem vyhodnocován.
xp_ Používá se na začátku názvu uložených procedur rozšířených katalogem, například xp_cmdshell.

Použijte typově bezpečné parametry SQL

Kolekce Parameters v databázovém stroji poskytuje kontrolu typů a ověřování délky. Pokud používáte kolekci Parameters , vstup se považuje za literálovou hodnotu místo jako spustitelný kód. Další výhodou použití Parameters kolekce je, že můžete provádět kontroly typu a délky dat. Hodnoty mimo rozsah aktivují výjimku. Následující fragment kódu ukazuje použití Parameters kolekce:

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;

V tomto příkladu @au_id se parametr považuje za hodnotu literálu namísto spustitelného kódu. Tato hodnota se kontroluje z hlediska typu a délky. Pokud hodnota @au_id nevyhovuje zadanému typu a omezením délky, vyvolá se výjimka.

Použití parametrizovaného vstupu s uloženými procedurami

Uložené procedury můžou být náchylné k injektáži SQL, pokud používají nefiltrovaný vstup. Například následující kód je zranitelný:

SqlDataAdapter myCommand =
    new SqlDataAdapter("LoginStoredProcedure '" + Login.Text + "'", conn);

Pokud používáte uložené procedury, měli byste jako vstup použít parametry.

Použijte kolekci Parametrů s dynamickým SQL

Pokud nemůžete použít uložené procedury, můžete přesto použít parametry, jak je znázorněno v následujícím příkladu kódu.

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;

Filtrování vstupu

Filtrování vstupu může být také užitečné při ochraně před injektáží SQL odebráním řídicích znaků. Filtrování však vzhledem k velkému počtu znaků, které můžou představovat problémy, není spolehlivou obranou. Následující příklad vyhledá oddělovač znaků.

private string SafeSqlLiteral(string inputSQL)
{
    return inputSQL.Replace("'", "''");
}

Klauzule LIKE

Pokud použijete LIKE klauzuli, musí být zástupné znaky stále uchycené:

s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");

Kontrola kódu pro injektáž SQL

Měli byste zkontrolovat veškerý kód, který volá EXECUTE, EXECnebo sp_executesql. S identifikací postupů, které obsahují tyto příkazy, můžete použít podobné dotazy jako v následujícím příkladu. Tento dotaz zkontroluje 1, 2, 3 nebo 4 mezery za slovy EXECUTE nebo 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%';

Zabalte parametry pomocí QUOTENAME() a REPLACE()

V každé vybrané uložené proceduře ověřte, zda jsou všechny proměnné použité v dynamickém Transact-SQL zpracovány správně. Data, která pocházejí ze vstupních parametrů uložené procedury nebo která jsou načtena z tabulky, by měla být uzavřena v QUOTENAME() nebo REPLACE(). Mějte na paměti, že hodnota @variable , která je předána QUOTENAME() , je sysname a má maximální délku 128 znaků.

@variable Doporučený obal
Název zabezpečeného QUOTENAME(@variable)
Řetězec = <128 znaků QUOTENAME(@variable, '''')
> Řetězec 128 znaků REPLACE(@variable,'''', '''''')

Při použití této techniky SET lze příkaz revidovat následujícím způsobem:

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

Vkládání umožněno zkrácením dat

Všechny dynamické Transact-SQL, které jsou přiřazeny proměnné, jsou zkráceny, pokud jsou větší než buffer přidělený této proměnné. Útočník, který dokáže vynutit zkrácení příkazu tím, že předá uložené proceduře neočekávaně dlouhé řetězce, může manipulovat s výsledkem. Například v následujícím příkladu je uložený postup ohrožen injektováním umožněným zkrácením.

V tomto příkladu máme vyrovnávací paměť s maximální délkou @command 200 znaků. K nastavení hesla 'sa'příkazu potřebujeme celkem 154 znaků: 26, UPDATE 16 pro WHERE klauzuli, 4 pro 'sa'a 2 pro uvozovky obklopené QUOTENAME(@loginname): 200 - 26 - 16 - 4 - 2 = 154. Protože @new je ale deklarován jako název systému, může tato proměnná obsahovat pouze 128 znaků. Můžeme to překonat předáním některých jednoduchých uvozovek do @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

Pokud útočník předá 154 znaků do vyrovnávací paměti 128 znaků, může nastavit nové heslo sa , aniž by znal staré heslo.

EXEC sp_MySetPassword 'sa',
    'dummy',
    '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''

Z tohoto důvodu byste měli použít velkou vyrovnávací paměť pro proměnnou příkazu nebo přímo spustit dynamické Transact-SQL uvnitř EXECUTE příkazu.

Zkrácení při použití QUOTENAME(@variable, '''') a REPLACE()

Řetězce vrácené QUOTENAME() a REPLACE() jsou tiše zkráceny, pokud překročí přidělený prostor. Uložená procedura vytvořená v následujícím příkladu ukazuje, co se může stát.

V tomto případě jsou data uložená v dočasných proměnných zkrácena, protože velikost @login, @oldpassword, a @newpassword je pouze 128 znaků, ale QUOTENAME() může vrátit až 258 znaků. Pokud @new obsahuje 128 znaků, @newpassword může být 123... n, kde n je 127. znak. Protože je řetězec vrácený QUOTENAME() zkrácený, lze upravit tak, aby vypadal jako následující prohlášení:

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

Následující příkaz proto nastaví hesla všech uživatelů na hodnotu, která byla předána v předchozím kódu.

EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'

Zkrácení řetězce můžete vynutit překročením přiděleného prostoru vyrovnávací paměti při použití REPLACE(). Uložená procedura vytvořená v následujícím příkladu ukazuje, co se může stát.

V tomto příkladu jsou data zkrácena, protože vyrovnávací paměti přidělené @login@oldpassword a @newpassword mohou obsahovat pouze 128 znaků, ale QUOTENAME() mohou vrátit až 258 znaků. Pokud @new obsahuje 128 znaků, @newpassword může být '123...n', kde n je 127. znak. Protože je řetězec vrácený QUOTENAME() zkrácený, lze upravit tak, aby vypadal jako následující prohlášení:

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

Stejně jako u QUOTENAME(), zkrácení řetězce pomocí REPLACE() lze zabránit deklarováním dočasných proměnných, které jsou dostatečně velké pro všechny případy. Pokud je to možné, měli byste volat QUOTENAME() nebo REPLACE() přímo uvnitř dynamického jazyka Transact-SQL. V opačném případě můžete vypočítat požadovanou velikost vyrovnávací paměti následujícím způsobem. Pro @outbuffer = QUOTENAME(@input), velikost @outbuffer by měla být 2 * (len(@input) + 1). Při použití REPLACE() a dvojitých uvozovek, stejně jako v předchozím příkladu, je dostatečná vyrovnávací paměť 2 * len(@input).

Následující výpočet zahrnuje všechny případy:

WHILE LEN(@find_string) > 0, required buffer size =
    ROUND(LEN(@input) / LEN(@find_string), 0)
        * LEN(@new_string) + (LEN(@input) % LEN(@find_string))

Zkrácení při použití QUOTENAME(@variable, ']')

Zkrácení může nastat, když je název zabezpečitelného objektu databázového stroje předán příkazům, které používají formulář QUOTENAME(@variable, ']'). Následující příklad ukazuje tento scénář.

V tomto příkladu @objectname musí být povoleno 2 * 258 + 1 znaků.

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

Při zřetězení hodnot typu sysname byste měli použít dočasné proměnné dostatečně velké, aby měly maximální počet 128 znaků na hodnotu. Pokud je to možné, volejte QUOTENAME() přímo uvnitř dynamického jazyka Transact-SQL. V opačném případě můžete vypočítat požadovanou velikost vyrovnávací paměti, jak je vysvětleno v předchozí části.