Co je SQL Injection a jak tomu zabránit v PHP aplikacích?

Takže si myslíte, že vaše databáze SQL je výkonná a bezpečná před okamžitým zničením? No, SQL Injection nesouhlasí!

Ano, mluvíme o okamžitém zničení, protože nechci otevírat tento článek obvyklou chabou terminologií „zpřísnění zabezpečení“ a „zabránění škodlivému přístupu“. SQL Injection je tak starý trik v knize, že o něm každý, každý vývojář velmi dobře ví a dobře ví, jak tomu předejít. Kromě toho jednoho zvláštního okamžiku, kdy uklouznou, a výsledky mohou být katastrofální.

Pokud už víte, co je SQL Injection, klidně přeskočte na druhou polovinu článku. Ale pro ty, kteří se v oblasti vývoje webu teprve chystají a sní o tom, že převezmou vyšší role, je na místě nějaký úvod.

Co je SQL Injection?

Klíč k pochopení SQL Injection je v jeho názvu: SQL + Injection. Slovo „injekce“ zde nemá žádné lékařské konotace, ale spíše se jedná o použití slovesa „injekce“. Společně tato dvě slova vyjadřují myšlenku vložení SQL do webové aplikace.

Vložení SQL do webové aplikace. . . hmmm . . Není to to, co stejně děláme? Ano, ale nechceme, aby naši databázi řídil útočník. Pochopme to pomocí příkladu.

Řekněme, že vytváříte typický PHP web pro místní e-shop, takže se rozhodnete přidat kontaktní formulář, jako je tento:

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

A předpokládejme, že soubor send_message.php ukládá vše do databáze, aby si majitelé obchodu mohli později číst uživatelské zprávy. Může to mít nějaký kód, jako je tento:

<?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

Nejprve se tedy pokoušíte zjistit, zda tento uživatel již nemá nepřečtenou zprávu. Dotaz SELECT * ze zpráv, kde jméno = $jméno vypadá dost jednoduše, že?

ŠPATNĚ!

Ve své nevině jsme otevřeli dveře okamžitému zničení naší databáze. Aby se tak stalo, musí útočník splnit následující podmínky:

  • Aplikace běží na SQL databázi (dnes téměř každá aplikace)
  • Aktuální databázové připojení má v databázi oprávnění „upravit“ a „odstranit“.
  • Názvy důležitých tabulek lze uhodnout

Třetí bod znamená, že nyní, když útočník ví, že provozujete e-shop, velmi pravděpodobně ukládáte data objednávek do tabulky objednávek. Vyzbrojen tím vším, stačí, aby útočník uvedl toto jako své jméno:

  Jak opravit Slow Touch ID na vašem iPhone

Joe; zkrátit objednávky; Ano, pane! Podívejme se, jaký bude dotaz, když bude spuštěn skriptem PHP:

SELECT * FROM zprávy WHERE jméno = Joe; zkrátit objednávky;

Dobře, první část dotazu obsahuje chybu syntaxe (bez uvozovek kolem „Joe“), ale středník přinutí motor MySQL, aby začal interpretovat nový: zkrátit příkazy. Jen tak jedním šmahem je celá historie objednávek pryč!

Nyní, když víte, jak SQL Injection funguje, je čas podívat se, jak ji zastavit. Dvě podmínky, které musí být splněny pro úspěšnou injekci SQL, jsou:

  • PHP skript by měl mít oprávnění k úpravě/mazání databáze. Myslím, že to platí pro všechny aplikace a nebudete moci nastavit své aplikace pouze pro čtení. 🙂 A hádejte co, i když odebereme všechna oprávnění pro úpravy, SQL injection může stále někomu umožnit spouštět SELECT dotazy a prohlížet celou databázi, včetně citlivých dat. Jinými slovy, snížení úrovně přístupu k databázi nefunguje a vaše aplikace to stejně potřebuje.
  • Vstup uživatele se zpracovává. Jediný způsob, jak může SQL injection fungovat, je, když přijímáte data od uživatelů. Opět není praktické zastavit všechny vstupy pro vaši aplikaci jen proto, že se obáváte injekce SQL.
  • Zabránění vkládání SQL v PHP

    Nyní, vzhledem k tomu, že databázová připojení, dotazy a uživatelské vstupy jsou součástí života, jak zabráníme vkládání SQL? Naštěstí je to docela jednoduché a existují dva způsoby, jak to udělat: 1) dezinfikovat uživatelský vstup a 2) použít připravené příkazy.

    Dezinfikujte uživatelský vstup

    Pokud používáte starší verzi PHP (5.5 nebo nižší, a to se často stává na sdíleném hostingu), je rozumné spouštět všechny vaše uživatelské vstupy prostřednictvím funkce nazvané mysql_real_escape_string(). V podstatě to, co to dělá, odstraní všechny speciální znaky v řetězci, takže při použití databází ztratí svůj význam.

    Pokud máte například řetězec jako já jsem řetězec, může útočník použít znak jednoduché uvozovky (‚) k manipulaci s vytvářeným databázovým dotazem a způsobit vložení SQL. Spuštěním pomocí mysql_real_escape_string() vytvoříme řetězec I’m, který přidá zpětné lomítko k jednoduché uvozovce, čímž ji unikne. Výsledkem je, že celý řetězec je nyní předán jako neškodný řetězec do databáze, místo aby se mohl účastnit manipulace s dotazem.

    Tento přístup má jednu nevýhodu: je to opravdu, opravdu stará technika, která jde spolu se staršími formami přístupu k databázi v PHP. Od PHP 7 tato funkce již ani neexistuje, což nás přivádí k našemu dalšímu řešení.

      Jak odstranit hluk na pozadí ze zvuku [10 Tools]

    Použijte připravená prohlášení

    Připravené příkazy představují způsob, jak provádět databázové dotazy bezpečněji a spolehlivěji. Myšlenka je taková, že místo odesílání nezpracovaného dotazu do databáze nejprve sdělíme databázi strukturu dotazu, který budeme posílat. To je to, co máme na mysli „přípravou“ prohlášení. Jakmile je výpis připraven, předáme informace jako parametrizované vstupy, takže databáze může „zaplnit mezery“ tím, že zapojí vstupy do struktury dotazu, kterou jsme předtím odeslali. To odebere jakýkoli zvláštní výkon, který mohou mít vstupy, což způsobí, že se s nimi bude v celém procesu zacházet jako s pouhými proměnnými (nebo užitečnými zatíženími, chcete-li). Takto vypadají připravená prohlášení:

    <?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();
    ?>

    Vím, že tento proces zní zbytečně složitě, pokud s připravenými prohlášeními začínáte, ale tento koncept stojí za námahu. Tady je pěkný úvod do toho.

    Pro ty, kteří již znají rozšíření PDO v PHP a jeho použití k vytváření připravených výpisů, mám malou radu.

    Upozornění: Při nastavování PDO buďte opatrní

    Při použití PDO pro přístup k databázi se můžeme dostat do falešného pocitu bezpečí. „Aha, já používám PDO.“ Teď už nepotřebuji myslet na nic jiného“ — takto obecně probíhá naše myšlení. Je pravda, že PDO (nebo MySQLi připravené příkazy) stačí k tomu, aby se zabránilo všemožným útokům SQL injection, ale musíte být opatrní při jeho nastavování. Je běžné pouze zkopírovat a vložit kód z výukových programů nebo z vašich dřívějších projektů a pokračovat, ale toto nastavení může vrátit zpět vše:

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

    Toto nastavení slouží k tomu, aby PDO sdělilo, aby emulovalo připravené příkazy, spíše než aby skutečně použilo funkci připravených příkazů databáze. V důsledku toho PHP posílá jednoduché řetězce dotazů do databáze, i když váš kód vypadá, že vytváří připravené příkazy a nastavuje parametry a tak dále. Jinými slovy, jste stejně zranitelní vůči SQL injection jako dříve. 🙂

    Řešení je jednoduché: ujistěte se, že je tato emulace nastavena na hodnotu false.

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

    Nyní je PHP skript nucen používat připravené příkazy na databázové úrovni, což zabraňuje všem druhům SQL injection.

      Co je síťová brána firewall a jak pomáhá zastavit útoky?

    Prevence pomocí WAF

    Víte, že webové aplikace můžete také chránit před SQL injection pomocí WAF (web application firewall)?

    No, nejen SQL injection, ale mnoho dalších zranitelností vrstvy 7, jako je skriptování mezi weby, nefunkční autentizace, padělání mezi weby, vystavení dat atd. Buď můžete použít vlastní hostování, jako je Mod Security, nebo cloudové, jak je uvedeno níže.

    SQL injection a moderní PHP frameworky

    Injekce SQL je tak běžná, tak snadná, tak frustrující a tak nebezpečná, že všechny moderní webové rámce PHP mají zabudovaná protiopatření. Ve WordPressu máme například funkci $wpdb->prepare(), zatímco pokud používáte framework MVC, udělá všechnu špinavou práci za vás a nemusíte ani myslet na to, abyste zabránili vkládání SQL. Je trochu nepříjemné, že ve WordPressu musíte připravovat prohlášení výslovně, ale hej, mluvíme o WordPressu. 🙂

    Každopádně mi jde o to, že moderní plemeno webových vývojářů nemusí přemýšlet o SQL injection, a v důsledku toho si tuto možnost ani neuvědomují. I když ve své aplikaci nechají otevřená jedna zadní vrátka (možná je to parametr dotazu $_GET a staré zvyky odpálit špinavý dotaz), výsledky mohou být katastrofální. Vždy je tedy lepší najít si čas a ponořit se hlouběji do základů.

    Závěr

    SQL Injection je velmi ošklivý útok na webovou aplikaci, ale lze se mu snadno vyhnout. Jak jsme viděli v tomto článku, být opatrný při zpracování uživatelského vstupu (mimochodem, SQL Injection není jedinou hrozbou, kterou manipulace s uživatelským vstupem přináší) a dotazování na databázi je vše, co je potřeba. To znamená, že ne vždy pracujeme v zabezpečení webového rámce, takže je lepší si být vědomi tohoto typu útoku a nenaletět mu.

    x