Dela via


SQL-injektion

gäller för:SQL ServerAzure SQL DatabaseAzure SQL Managed InstanceAzure Synapse AnalyticsAnalytics Platform System (PDW)SQL-databas i Microsoft Fabric

SQL-inmatning är ett angrepp där skadlig kod infogas i strängar som senare skickas till en instans av SQL Server Database Engine för parsning och körning. Alla procedurer som konstruerar SQL-instruktioner bör granskas för sårbarheter vid inmatning, eftersom databasmotorn kör alla syntaktiskt giltiga frågor som den tar emot. Även parametriserade data kan manipuleras av en skicklig och bestämd angripare.

Så här fungerar SQL-inmatning

Den primära formen av SQL-inmatning består av direkt infogning av kod i användarindatavariabler som sammanfogas med SQL-kommandon och körs. En mindre direkt attack matar in skadlig kod i strängar som är avsedda för lagring i en tabell eller som metadata. När de lagrade strängarna sedan sammanfogas till ett dynamiskt SQL-kommando körs den skadliga koden.

Inmatningsprocessen fungerar genom att en textsträng avslutas i förtid och ett nytt kommando läggs till. Eftersom det infogade kommandot kan ha extra strängar tillagda innan det körs, avslutar malefactor den inmatade strängen med ett kommentarstecken --. Efterföljande text ignoreras vid körning.

Följande skript visar en enkel SQL-inmatning. Skriptet skapar en SQL-fråga genom att sammanfoga hårdkodade strängar tillsammans med en sträng som användaren anger:

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

Användaren uppmanas att ange namnet på en stad. Om de anger Redmondser frågan som sammanställts av skriptet ut ungefär som i följande exempel:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

Anta dock att användaren anger följande text:

Redmond';drop table OrdersTable--

I det här fallet sammanställer skriptet följande fråga:

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

Semikolonet (;) anger slutet på en fråga och början av en annan. Det dubbla bindestrecket (--) anger att resten av den aktuella raden är en kommentar och bör ignoreras. Om den ändrade koden är syntaktiskt korrekt körs den av servern. När databasmotorn bearbetar den här instruktionen väljer den först alla poster i OrdersTable där ShipCity är Redmond. Sedan släpper databasmotorn OrdersTable.

Så länge inmatad SQL-kod är syntaktiskt korrekt kan manipulering inte identifieras programmatiskt. Därför måste du verifiera alla användarindata och noggrant granska kod som kör konstruerade SQL-kommandon på den server som du använder. Metodtips för kodning beskrivs i följande avsnitt i den här artikeln.

Verifiera alla indata

Verifiera alltid användarindata genom att testa typ, längd, format och intervall. När du vidtar försiktighetsåtgärder mot skadliga indata bör du överväga arkitektur- och distributionsscenarierna för ditt program. Kom ihåg att program som är utformade för att köras i en säker miljö kan kopieras till en icke-säker miljö. Följande förslag bör betraktas som metodtips:

  • Gör inga antaganden om storleken, typen eller innehållet för de data som tas emot av ditt program. Du bör till exempel göra följande utvärdering:

    • Hur fungerar ditt program om en felaktig eller skadlig användare anger en videofil på 2 GB där ditt program förväntar sig ett postnummer?

    • Hur fungerar programmet om en DROP TABLE instruktion är inbäddad i ett textfält?

  • Testa indatatypens storlek och datatyp och tillämpa lämpliga gränser. Detta kan bidra till att förhindra avsiktliga buffertöverskridanden.

  • Testa innehållet i strängvariabler och acceptera endast förväntade värden. Avvisa poster som innehåller binära data, escape-sekvenser och kommentarstecken. Detta kan hjälpa till att förhindra skriptinmatning och kan skydda mot vissa buffertöverskridningar.

  • När du arbetar med XML-dokument verifierar du alla data mot schemat när de anges.

  • Skapa aldrig Transact-SQL-instruktioner direkt från användarindata.

  • Använd lagrade procedurer för att verifiera användarindata.

  • I miljöer med flera nivåer bör alla data verifieras innan de tas emot i den betrodda zonen. Data som inte klarar valideringsprocessen bör avvisas och ett fel ska returneras till den föregående nivån.

  • Implementera flera valideringslager. Försiktighetsåtgärder som du vidtar mot tillfälligt skadliga användare kan vara ineffektiva mot beslutsamma angripare. En bättre metod är att verifiera indata i användargränssnittet och vid alla efterföljande punkter där den korsar en förtroendegräns.

    Dataverifiering i ett program på klientsidan kan till exempel förhindra enkel skriptinmatning. Men om nästa nivå förutsätter att dess indata redan har verifierats kan alla skadliga användare som kan kringgå en klient ha obegränsad åtkomst till ett system.

  • Sammanfoga aldrig användarindata som inte har verifierats. Strängsammanfogning är den primära startpunkten för skriptinmatning.

  • Acceptera inte följande strängar i fält som filnamn kan konstrueras från: AUX, , COM1CLOCK$till COM8, CON, CONFIG$, LPT1 till och med LPT8, NULoch PRN.

Avvisa om möjligt indata som innehåller följande tecken.

Indatatecken Betydelse i Transact-SQL
; Frågegränsare.
' Avgränsare för teckendatasträngar.
-- Avgränsare för enradskommentarer. Text som följer -- till slutet av raden utvärderas inte av servern.
/*** ... ***/ Kommentera avgränsare. Text mellan /* och */ utvärderas inte av servern.
xp_ Används i början av namnet på katalogförlängda lagrade procedurer, till exempel xp_cmdshell.

Använda typsäkra SQL-parametrar

Samlingen Parameters i databasmotorn innehåller typkontroll och längdvalidering. Om du använder samlingen Parameters behandlas indata som ett literalvärde i stället för som körbar kod. En annan fördel med att använda Parameters samlingen är att du kan tillämpa typ- och längdkontroller. Värden utanför intervallet utlöser ett undantag. Följande kodfragment visar hur samlingen Parameters används:

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;

I det här exemplet behandlas parametern @au_id som ett literalvärde i stället för som körbar kod. Det här värdet är kontrollerat för typ och längd. Om värdet @au_id inte överensstämmer med den angivna typen och längdbegränsningarna, genereras ett undantag.

Använda parametriserade indata med lagrade procedurer

Lagrade procedurer kan vara känsliga för SQL-inmatning om de använder ofiltrerade indata. Följande kod är till exempel sårbar:

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

Om du använder lagrade procedurer bör du använda parametrar som indata.

Använd samlingen Parametrar med dynamisk SQL

Om du inte kan använda lagrade procedurer kan du fortfarande använda parametrar, som du ser i följande kodexempel.

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;

Filtrera indata

Filtreringsindata kan också vara till hjälp för att skydda mot SQL-inmatning genom att ta bort escape-tecken. Men på grund av det stora antalet tecken som kan orsaka problem är filtrering inte ett tillförlitligt skydd. I följande exempel söker du efter avgränsaren för teckensträngen.

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

LIKE-satser

Om du använder en LIKE sats måste jokertecken fortfarande vara undantagna:

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

Granska kod för SQL-inmatning

Du bör granska all kod som anropar EXECUTE, EXECeller sp_executesql. Du kan använda frågor som liknar följande för att identifiera procedurer som innehåller dessa instruktioner. Den här frågan söker efter 1, 2, 3 eller 4 blanksteg efter orden EXECUTE eller 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%';

Omsluta parametrar med QUOTENAME() och REPLACE()

I varje vald lagrad procedur kontrollerar du att alla variabler som används i dynamiska Transact-SQL hanteras korrekt. Data som kommer från indataparametrarna för den lagrade proceduren eller som läss från en tabell ska omslutas i QUOTENAME() eller REPLACE(). Kom ihåg att värdet för @variable som skickas till QUOTENAME() är sysname och har en maximal längd på 128 tecken.

@variable Rekommenderad omslutning
Namn på ett skyddbart objekt QUOTENAME(@variable)
Sträng av <= 128 tecken QUOTENAME(@variable, '''')
Sträng med > 128 tecken REPLACE(@variable,'''', '''''')

När du använder den här tekniken kan en SET instruktion ändras på följande sätt:

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

Injektion möjliggörs av datatrunkering

Alla dynamiska Transact-SQL som har tilldelats till en variabel trunkeras om den är större än bufferten som allokerats för variabeln. En angripare som kan tvinga på trunkering av ett uttalande genom att skicka oväntat långa strängar till en lagrad procedur kan manipulera resultatet. Till exempel är följande exempel på lagrad procedur sårbart för injektion som möjliggörs av trunkering.

I det här exemplet har vi en @command buffert med en maximal längd på 200 tecken. Vi behöver totalt 154 tecken för att ange lösenordet för 'sa': 26 för UPDATE-instruktionen, 16 för WHERE-satsen, 4 för 'sa', och 2 för citattecken omgivna av QUOTENAME(@loginname): 200 - 26 - 16 - 4 - 2 = 154. Men eftersom @new den deklareras som sysname kan den här variabeln bara innehålla 128 tecken. Vi kan lösa detta genom att skicka några enkla citattecken i @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

Om en angripare skickar 154 tecken till en buffert på 128 tecken kan de ange ett nytt lösenord utan sa att känna till det gamla lösenordet.

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

Därför bör du använda en stor buffert för en kommandovariabel eller köra den dynamiska Transact-SQL direkt i -instruktionen EXECUTE .

Trunkering när QUOTENAME(@variable, '''') och REPLACE() används

Strängar som returneras av QUOTENAME() och REPLACE() trunkeras tyst om de överskrider det allokerade utrymmet. Den lagrade proceduren som skapas i följande exempel visar vad som kan hända.

I det här exemplet trunkeras data som lagras i tillfälliga variabler, eftersom buffertstorleken @login, @oldpasswordoch @newpassword endast är 128 tecken, men QUOTENAME() kan returnera upp till 258 tecken. Om @new innehåller 128 tecken kan det @newpassword vara 123... n, där n är det 127:e tecknet. Eftersom strängen som returneras av QUOTENAME() trunkeras kan den göras så att den ser ut så här:

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

Därför anger följande instruktion lösenorden för alla användare till det värde som skickades i föregående kod.

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

Du kan framtvinga strängtrumning genom att överskrida det allokerade buffertutrymmet när du använder REPLACE(). Den lagrade proceduren som skapas i följande exempel visar vad som kan hända.

I det här exemplet trunkeras data eftersom buffertarna som allokerats för @login, @oldpassword och @newpassword endast kan innehålla 128 tecken, men QUOTENAME() kan returnera upp till 258 tecken. Om @new innehåller 128 tecken @newpassword kan vara '123...n', där n är det 127:e tecknet. Eftersom strängen som returneras av QUOTENAME() trunkeras kan den göras så att den ser ut så här:

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

Precis som med QUOTENAME()kan du undvika strängtrunkering efter REPLACE() genom att deklarera tillfälliga variabler som är tillräckligt stora för alla fall. När det är möjligt bör du anropa QUOTENAME() eller REPLACE() direkt i den dynamiska Transact-SQL. Annars kan du beräkna den buffertstorlek som krävs enligt följande. För @outbuffer = QUOTENAME(@input) ska storleken på @outbuffer vara 2 * (len(@input) + 1). När du använder REPLACE() och dubbla citattecken, som i det föregående exemplet, räcker det med en buffert på 2 * len(@input).

Följande beräkning omfattar alla fall:

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

Trunkering när QUOTENAME(@variable, ']') används

Avkortning kan inträffa när namnet på ett säkerhetsobjekt för databasmotorn överförs till uttalanden som använder formen QUOTENAME(@variable, ']'). I följande exempel visas det här scenariot.

I det här exemplet @objectname måste du tillåta 2 * 258 + 1 tecken.

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

När du sammanfogar värden av typen sysname bör du använda temporära variabler som är tillräckligt stora för att innehålla högst 128 tecken per värde. Om möjligt anropar du QUOTENAME() direkt i den dynamiska Transact-SQL. Annars kan du beräkna den buffertstorlek som krävs enligt beskrivningen i föregående avsnitt.