Vad är SQL Injection och hur man förhindrar i PHP-applikationer?

Så du tror att din SQL-databas är effektiv och säker från omedelbar förstörelse? Nåväl, SQL Injection håller inte med!

Ja, det är omedelbar förstörelse vi pratar om, eftersom jag inte vill öppna den här artikeln med den vanliga lata terminologin ”att skärpa säkerheten” och ”förhindra skadlig åtkomst.” SQL Injection är ett så gammalt trick i boken att alla, varje utvecklare, känner till det mycket väl och är väl medvetna om hur man kan förhindra det. Förutom den där ena udda gången när de halkar upp, och resultaten kan bli inget annat än katastrofala.

Om du redan vet vad SQL Injection är, hoppa gärna till den senare hälften av artikeln. Men för de som precis kommer inom området webbutveckling och drömmer om att ta sig an fler seniora roller är en introduktion på sin plats.

Vad är SQL Injection?

Nyckeln till att förstå SQL Injection ligger i dess namn: SQL + Injection. Ordet ”injektion” här har inga medicinska konnotationer, utan är snarare användningen av verbet ”injicera.” Tillsammans förmedlar dessa två ord tanken att lägga in SQL i en webbapplikation.

Lägga in SQL i en webbapplikation. . . hmmm . . . Är det inte det vi gör ändå? Ja, men vi vill inte att en angripare ska driva vår databas. Låt oss förstå det med hjälp av ett exempel.

Låt oss säga att du bygger en typisk PHP-webbplats för en lokal e-handelsbutik, så du bestämmer dig för att lägga till ett kontaktformulär så här:

<form action="record_message.php" method="POST">
  <label>Your name</label>
  <input type="text" name="name">
  
  <label>Your message</label>
  <textarea name="message" rows="5"></textarea>
  
  <input type="submit" value="Send">
</form>

Och låt oss anta att filen send_message.php lagrar allt i en databas så att butiksägarna kan läsa användarmeddelanden senare. Den kan ha någon kod så här:

<?php

$name = $_POST['name'];
$message = $_POST['message'];

// check if this user already has a message
mysqli_query($conn, "SELECT * from messages where name = $name");

// Other code here

Så du försöker först se om den här användaren redan har ett oläst meddelande. Frågan SELECT * från meddelanden där namn = $namn verkar enkel nog, eller hur?

FEL!

I vår oskuld har vi öppnat dörrarna för omedelbar förstörelse av vår databas. För att detta ska hända måste angriparen ha följande villkor uppfyllda:

  • Applikationen körs på en SQL-databas (idag är nästan alla applikationer)
  • Den aktuella databasanslutningen har ”redigera” och ”radera”-behörigheter i databasen
  • Namnen på de viktiga tabellerna kan gissas

Den tredje punkten innebär att nu när angriparen vet att du driver en e-handelsbutik, lagrar du mycket troligt orderdata i en ordertabell. Beväpnad med allt detta behöver angriparen bara ange detta som sitt namn:

Joe; trunkera order;? Ja, sir! Låt oss se vad frågan kommer att bli när den exekveras av PHP-skriptet:

SELECT * FROM meddelanden WHERE name = Joe; trunkera order;

Okej, den första delen av frågan har ett syntaxfel (inga citattecken runt ”Joe”), men semikolonet tvingar MySQL-motorn att börja tolka en ny: trunkera order. Bara så, i ett enda svep är hela orderhistoriken borta!

Nu när du vet hur SQL Injection fungerar är det dags att titta på hur du stoppar det. De två villkoren som måste uppfyllas för framgångsrik SQL-injektion är:

  • PHP-skriptet bör ha behörighet att ändra/ta bort i databasen. Jag tror att detta är sant för alla applikationer och du kommer inte att kunna göra dina applikationer skrivskyddade. 🙂 Och gissa vad, även om vi tar bort alla modifieringsprivilegier, kan SQL-injektion fortfarande tillåta någon att köra SELECT-frågor och se all databaser, inklusive känsliga data. Med andra ord, att minska databasåtkomstnivån fungerar inte, och din applikation behöver det ändå.
  • Användarinmatning bearbetas. Det enda sättet som SQL-injektion kan fungera är när du accepterar data från användare. Återigen är det inte praktiskt att stoppa alla ingångar för din applikation bara för att du är orolig för SQL-injektion.
  • Förhindrar SQL-injektion i PHP

    Nu, med tanke på att databasanslutningar, frågor och användarinmatningar är en del av livet, hur förhindrar vi SQL-injektion? Tack och lov är det ganska enkelt, och det finns två sätt att göra det: 1) rensa användarinput och 2) använd förberedda uttalanden.

    Rensa användarinmatning

    Om du använder en äldre PHP-version (5.5 eller lägre, och detta händer mycket på delad hosting), är det klokt att köra all din användarinmatning genom en funktion som heter mysql_real_escape_string(). I grund och botten, vad det gör det tar bort alla specialtecken i en sträng så att de förlorar sin betydelse när de används av databasen.

    Till exempel, om du har en sträng som jag är en sträng, kan det enda citattecknet (’) användas av en angripare för att manipulera databasfrågan som skapas och orsaka en SQL-injektion. Att köra den genom mysql_real_escape_string() producerar I’m a string, som lägger till ett snedstreck till det enstaka citatet och undviker det. Som ett resultat skickas hela strängen nu som en ofarlig sträng till databasen, istället för att kunna delta i frågemanipulation.

    Det finns en nackdel med detta tillvägagångssätt: det är en riktigt, riktigt gammal teknik som går ihop med de äldre formerna av databasåtkomst i PHP. Från och med PHP 7 finns den här funktionen inte ens längre, vilket för oss till vår nästa lösning.

    Använd förberedda uttalanden

    Förberedda uttalanden är ett sätt att göra databasfrågor säkrare och tillförlitligare. Tanken är att istället för att skicka den råa frågan till databasen, berättar vi först för databasen strukturen för den fråga vi kommer att skicka. Detta är vad vi menar med att ”förbereda” ett uttalande. När ett uttalande har förberetts skickar vi informationen som parametriserade indata så att databasen kan ”fylla luckorna” genom att koppla in ingångarna till frågestrukturen vi skickade tidigare. Detta tar bort all speciell kraft som ingångarna kan ha, vilket gör att de behandlas som enbart variabler (eller nyttolaster, om du så vill) under hela processen. Så här ser förberedda uttalanden ut:

    <?php
    $servername = "localhost";
    $username = "username";
    $password = "password";
    $dbname = "myDB";
    
    // Create connection
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // Check connection
    if ($conn->connect_error) {
        die("Connection failed: " . $conn->connect_error);
    }
    
    // prepare and bind
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $firstname, $lastname, $email);
    
    // set parameters and execute
    $firstname = "John";
    $lastname = "Doe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Mary";
    $lastname = "Moe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Julie";
    $lastname = "Dooley";
    $email = "[email protected]";
    $stmt->execute();
    
    echo "New records created successfully";
    
    $stmt->close();
    $conn->close();
    ?>

    Jag vet att processen låter onödigt komplex om du är ny på förberedda uttalanden, men konceptet är väl värt ansträngningen. Här är en trevlig introduktion till det.

    För den som redan är bekant med PHPs PDO-tillägg och använder den för att skapa förberedda uttalanden, har jag ett litet råd.

    Varning: Var försiktig när du ställer in PDO

    När vi använder PDO för databasåtkomst kan vi sugas in i en falsk känsla av säkerhet. ”Ah, ja, jag använder PDO. Nu behöver jag inte tänka på något annat” — så här går vårt tänkande generellt. Det är sant att PDO (eller MySQLi preparerade uttalanden) räcker för att förhindra alla typer av SQL-injektionsattacker, men du måste vara försiktig när du ställer in den. Det är vanligt att bara kopiera och klistra in kod från tutorials eller från dina tidigare projekt och gå vidare, men den här inställningen kan ångra allt:

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

    Vad den här inställningen gör är att berätta för PDO att emulera förberedda uttalanden snarare än att faktiskt använda funktionen förberedda uttalanden i databasen. Följaktligen skickar PHP enkla frågesträngar till databasen även om din kod ser ut som att den skapar förberedda uttalanden och ställer in parametrar och allt det där. Med andra ord, du är lika sårbar för SQL-injektion som tidigare. 🙂

    Lösningen är enkel: se till att denna emulering är inställd på falsk.

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

    Nu tvingas PHP-skriptet använda förberedda satser på databasnivå, vilket förhindrar alla typer av SQL-injektion.

    Förhindra användning av WAF

    Vet du att du också kan skydda webbapplikationer från SQL-injektion genom att använda WAF (web application firewall)?

    Tja, inte bara SQL-injektion utan många andra sårbarheter i lager 7 som cross-site scripting, trasig autentisering, cross-site förfalskning, dataexponering, etc. Antingen kan du använda självvärd som Mod Security eller molnbaserat som följande.

    SQL-injektion och moderna PHP-ramverk

    SQL-injektionen är så vanlig, så enkel, så frustrerande och så farlig att alla moderna PHP-webbramverk är inbyggda med motåtgärder. I WordPress, till exempel, har vi $wpdb->prepare()-funktionen, medan om du använder ett MVC-ramverk gör det allt det smutsiga arbetet åt dig och du behöver inte ens tänka på att förhindra SQL-injektion. Det är lite irriterande att man i WordPress måste förbereda uttalanden explicit, men hallå, det är WordPress vi pratar om. 🙂

    Hur som helst, min poäng är att den moderna rasen av webbutvecklare inte behöver tänka på SQL-injektion, och som ett resultat är de inte ens medvetna om möjligheten. Som sådan, även om de lämnar en bakdörr öppen i sin applikation (kanske är det en $_GET frågeparameter och gamla vanor att skjuta igång en smutsig fråga), kan resultaten bli katastrofala. Så det är alltid bättre att ta sig tid att dyka djupare ner i grunden.

    Slutsats

    SQL Injection är en mycket otäck attack på en webbapplikation men är lätt att undvika. Som vi såg i den här artikeln, att vara försiktig när du bearbetar användarinmatning (förresten, SQL Injection är inte det enda hotet som hantering av användarinmatning medför) och frågar efter databasen är allt som behövs. Som sagt, vi arbetar inte alltid med säkerheten i ett webbramverk, så det är bättre att vara medveten om den här typen av attacker och inte falla för den.