Unclosed quotation mark - ett vanligt säkerhetsproblem
Förord
Har ni någonsin råkat ut för att när ni skriver in något i en textruta på en webbsida och sedan klickar OK (eller logga in el. dyl.) så får ni ett felmeddelande likt det här nedanför när sidan som formuläret postas till ska laddas? Jag har sett det massor av gånger på flera olika webbsiter, och när jag i den senaste veckan stötte på det på en av de webbsiter jag besöker mest av alla så bestämde jag mig för att förklara vad det är som inträffar och framförallt varför det kan vara ett säkerhetsproblem. I den här artikeln kommer jag att använda en inloggningssida som exempel, men det kan som sagt inträffa var som helst där man postar innehållet i en textbox till servern och denne skickar det vidare till databasen. Se även http://www.hedgate.net för andra artiklar och tips om SQL Server.Innehåll
»»
»
»
»
Relaterade artiklar
Vad händer?
Microsoft OLE DB Provider for SQL Server (0x80040E14)
Unclosed quotation mark before the character string 'textsträng'
Alternativt kan ni se något liknande det här:
Microsoft OLE DB Provider for SQL Server (0x80040E14)
Line 1: Incorrect syntax near 'sträng'.
Precis som felmeddelandet talar om så har SQL Server stött på en strängkonstant som inte är avslutad i den SQL sats som den försöker exekvera. Strängar skrivs ju i SQL Server (liksom i de flesta andra DBMS) inom enkla citationstecken ('). Om man inleder en sträng med ett citationstecken men inte avslutar den med ett nytt så får man ovanstående felmeddelande. Men i vårt exempel med en inloggningssida så sitter ju inte besökaren och skriver in en SQL sats för att logga in, så varför inträffar det då? Tänk er att nedanstående kod kommer ifrån den ASP-sida som tar emot formuläret från inloggningen. Ett vanligt sätt att sköta inloggning är då att skicka en SELECT-fråga till databasen med de inloggningsuppgifter som besökaren matat in, och om frågan returnerar något så är användarnamnet/lösenordet korrekt. Koden kan då se ut ungefär så här (OBS! Jag menar inte att man ska göra så som det görs i exemplet, utan jag beskriver längre ner i artikeln hur jag anser att man ska hantera detta):
<%
Dim con, rs, sql
Dim txtUsername, txtPassword
Set con = Server.CreateObject("ADODB.Connection")
con.Open "Provider=SQLOLEDB.1;Persist Security Information=No;
User Name=foo;Password=bar;Initial Catalog=databasnamn;
Source=servernamn"
Set rs = Server.CreateObject("ADODB.Recordset")
sql = "SELECT UserId, FirstName, LastName " & _
"FROM Users " & _
"WHERE UserName = '" & txtUsername & "' " & _
"AND Password = '" & txtPassword & "'"
rs.Open sql, con
If rs.EOF Then
Response.Write("Felaktig användarnamn och/eller lösenord")
Else
Response.Write("Välkommen " & rs.Fields("FirstName") & " " & rs.Fields("LastName"))
Session("userid") = rs.Fields("UserId")
End If
%>
Jaha, det här ser ju bra ut. Om användaren matar in felaktiga uppgifter så får vi ett tomt recordset tillbaka och matar hon in korrekta uppgifter så får vi användarid, förnamn och efternamn och personen är inloggad. SQL satsen som skickas till SQL Server och exekveras ser ut så här (om användaren loggat in med kalle som användarnamn och kula som lösenord):
SELECT UserId, FirstName, LastName FROM Users WHERE UserName = 'kalle' AND Password = 'kula'
Jag kan tänka mig att de gånger ni sett felmeddelandena i början av artikeln är i en av dessa två situationer:
- Ni sitter och utvecklar någon kod, t ex en ASP-sida eller ett VB-program, där ni ska skicka en SQL sats till databasen och gör något fel när ni ska konkatenera ihop SQL satsen med de textsträngar som kommer från någon textbox och deras citationstecken. Normalt hittar man ganska fort var man gjort fel (t ex genom att skriva ut SQL satsen som skickas till databasen och se vad som är fel i den) och kan rätta problemet.
- Den andra situationen är det ni som är användare. Ni matar in ert användarnamn och lösenord och trycker på Return-tangenten för att logga in (fungerar på de flesta webbsidor som att klicka på inloggningsknappen). Det är bara det att precis innan ni trycker ner Return så råkar ni komma åt den lilla tangenten precis till vänster om Return, den med en asterisk (*) och ett enkelt citationstecken (') på. Det innebär att ni har nu fyllt i användarnamn kalle och lösenord kula'. SQL satsen som skickas till SQL Server kommer då att se ut så här:
SELECT UserId, FirstName, LastName
FROM Users
WHERE UserName = 'kalle' AND Password = 'kula''
Observera att det inte är ett dubbelt citationstecken (") i slutet utan två stycken enkla direkt efter varandra. Nu uppstår ett fel och ovanstående felmeddelande visas (beroende på vilken webbserver och vilka inställningar samt felhantering ni har så kan det naturligtvis visas andra felmeddelanden, men om ni skriver ut felmeddelandet från SQL Server är det ovanstående). Det första av felmeddelandena i artikelns början visas om man skrivit in citationstecknet sist i textrutan (dvs direkt intill det avslutande citationstecknet för strängen) och det andra visas om man skrivit citationstecknet någonstans mitt i strängen (t ex ku'la).
Säkerhetsproblem
Detta kanske inte tycks vara ett speciellt problem. Användaren misslyckas med sin inloggning, ett felmeddelande visas och hon får försöka igen, ingen skada skedd. Nej, men om användaren vill orsaka någon skada har de nu goda möjligheter. Tyvärr finns det ju personer som letar och utnyttjar säkerhetshål för att stjäla information eller bara sabotera. Beroende på säkerhetskonfigurationer, åtkomstmetod samt hur koden är skriven kan illvilliga användare orsaka mer eller mindre skada (eller i bästa fall ingen alls, och hur man ser till att så blir fallet kommer vi till längre ner). Tänk er att användaren skriver följande sträng i lösenordsrutan:
fubar' DELETE FROM Users --
Då kommer SQL satsen som skickas in till SQL Server att se ut så här:
SELECT UserId, FirstName, LastName FROM Users
WHERE UserName = 'kalle' AND Password = 'fubar' DELETE FROM Users --'
Här kan ni nog genast se problemet. Kommentarstecknen i slutet (--) kommer att se till att vad ni än lägger till i er SQL sats efter where-klausulen med lösenordet kommer att kommenteras bort, inklusive det avslutande citationstecknet. Eftersom användaren inkluderat ett citationstecken i lösenordssträngen kommer inte något fel att uppstå, strängen (fubar i detta fallet) avslutas ju korrekt. Därefter kommer en DELETE-sats vilken naturligtvis inte kan ingå i SELECT-satsen, så därför avslutas och exekveras SELECT-satsen där. Vare sig den returnerar något eller ej kommer därefter nästa sats att exekveras... Nu är det naturligtvis inte säkert att användaren vet namnet på någon av dina tabeller, men det lär inte ta särskilt många försök att gissa rätt. Det finns dessutom sätt att göra detta på utan att veta namnet på någon tabell i databasen.
Nu kommer vi in på 'beroende på säkerhetskonfigurationer ...'. Det absolut värsta scenariot man kan tänka sig är att ASP-koden kör som sa-användaren (system administrator) mot SQL Server (se mer nederst i artikeln under Råd om säkerhet) och SQL Server är inställd på att köra som en lokal eller domänanvändare med administratörsrättigheter på maskinen den kör på, alternativt Local System Account. Då har användaren fulla rättigheter att göra vad hon vill i SQL Server, samtidigt som SQL Server själv har fulla rättigheter att göra vad som helst på maskinen. Då finns det ingen begränsning för vad användaren kan åstadkomma.
Lösning
Hur ser man då till att detta inte kan ske på den webbsite/databas man utvecklar/administrerar? Den antagligen enklaste lösningen är att byta ut alla enkla citationstecken i de strängar som användarna skrivit in mot två stycken enkla citationstecken (alltså inte ett dubbelt citationstecken). SQL Server tolkar två enkla citationstecken i rad som tecknet citationstecken, inte som ett avslutande citationstecken på en sträng. Om man t ex ska skriva namnet O'Leary i en where-klausul skriver man WHERE LastName = 'O''Leary'. Alltså är det bara att köra Replace på de ord som man ska byta i, så att ASP-koden ser ut så här istället:
sql = "SELECT UserId, FirstName, LastName " & _
"FROM Users " & _
"WHERE UserName = '" & Replace(txtUsername, "'", "''") & "' " & _
"AND Password = '" & Replace(txtPassword, "'", "''") & "'"
Nu kommer alla enkla citationstecken som skrivits i textfälten, medvetet eller omedvetet, korrekt eller inte, att bytas ut mot två citationstecken. SQL Server kommer att tolka det som tecknet ', inte som ett avslutningstecken på en strängkonstant. Om användaren har försökt köra något 'extra' SQL kommando på detta sätt kommer inget att hända, förutom att inloggningen misslyckas (eftersom hon fyllt i ett felaktigt lösenord).
Denna metod löser alltså problemet, men som jag skrev i början av problemet tycker jag ändå inte detta sätt är ett bra sätt att skapa denna funktionalitet. Det är inte bara rent säkerhetsmässigt detta kan vara ett problem, även prestanda- och hanteringsmässigt kan det vara dåligt. Bygger man en applikation mot en databas bör man utnyttja lagrade procedurer, bl a av ovan nämnda skäl. Hade en lagrad procedur använts hade det också löst det diskuterade problemet, eftersom strängarna hade skickats som inparameter av strängtyp till proceduren. Då hade where-klausulen istället varit WHERE LastName = @lastname. SQL Server hade själv insett att variabeln @lastname är en strängvariabel och alltså ska behandlas som en sådan.
Råd om säkerhet
Först av allt, man kan säga det hur många gånger som helst, och alla håller med och förstår, men ändå ser man hela tiden hur alla struntar i det: Använd ALDRIG användaren sa för att logga in från en klientapplikation mot SQL Server med (självklart kan man göra det i Query Analyser etc). Det vanliga är att man använder det i början av utvecklingsfasen för att man inte orkar göra en ny användare då, sedan hinner man inte byta ut det senare och slutligen så har man installerat applikationen och den loggar fortfarande in som sa. Om en besökare nu hittar ett säkerhetshål hon kan utnyttja (exempelvis det som beskrivs i denna artikel) så kan de orsaka hur mycket förödelse som helst i systemet. Följ istället följande rekommendationer:- Direkt efter att databasen skapats, innan ni skapar den första tabellen, så skapar ni en inloggning på servern som ska användas av applikationen för att logga in, åtminstone temporärt. Sedan skapar ni i den aktuella databasen en användare som binds mot denna inloggning. När ni sedan skapar objekten i databasen så sätter ni de rättigheter som användaren behöver, per tabell, vy, procedur etc.
- Bäst säkerhet får man om man inte tillåter användaren (som applikationen loggar in med) att läsa/skriva direkt i tabellerna. Skapa istället lagrade procedurer för alla frågor mot och uppdateringar av tabellerna i databasen, och ge användaren exekveringsrättigheter på dessa. På så sätt kan inte användaren vare sig läsa eller skriva direkt i tabellerna även om de skulle utnyttja ett säkerhetshål liknande det som beskrivs i denna artikel.
- Om applikationen har olika typer av användare, t ex vissa som är administratörer och vissa endast besökare, så skapa olika inloggningar för dem i databasen. Endast administratörinloggningen kan exekvera de procedurer som uppdaterar databasen, medan de som endast läser data kan exekveras av alla.
- Se dock till att skapa alla tabeller och andra objekt med en användare som tillhör dbo-rollen, t ex sa-användaren. Annars måste alla objekt refereras till med kvalificerade namn (användare.objektnamn), vilket kan vara svårt om man inte vet vilken användare som skapat objektet.
Slutligen, även om det inte precis har något med det övriga innehållet i denna artikel, ett sista råd (egentligen nästan en befallning): Ha aldrig blankt lösenord på sa-användaren. Det går inte att säga det för många gånger, och trots det följs det ofta inte. Det finns ingen anledning att ha det blankt, inte ens på en utvecklingsmaskin.
/Christoffer Hedgate
0 Kommentarer