eKloe.com - Kubuntu User

Bezpečnost. Pár fíglů proti SQL Injection

by LucaS on Feb.06, 2009, under PHP

SQL Injection je druh útoku, který hackeři používají hodně často k získávání citlivých údajů z Vaší databáze. Ať už to jsou přihlašovací jména a hesla, přístup k administrátorskému rozhraní nebo prostě jen k znehodnocení databáze (smazání některých částí apod.). Přitom stačí tak málo. Ošetřit vstupní hodnoty.

Rozhodl jsem se napsat pár řádků na tohle téma. Hodně lidí se mi ptalo na řešení různých situací, ale jakmile se jim podaří spíchnout (naprogramovat) skript, který chtěli, už se nezajímají jak je tento skript bezpečný. Vždy je potřeba řešení podle způsobu použití nějak zabalit.

V čem je háček?
Dnes snad už každá webová aplikace, každé internetové stránky používají v jakékoliv míře databázi, ze které čerpají data (články, uživatele, …). Útočník využívá lennosti programátora a snaží se najít díru právě v těchto dotazích na databázi. Klasický dotaz lze ovlivnit vstupními hodnotami.

Pro příklad jednoduchý dotaz:

SELECT * FROM clanky WHERE id_clanek = 6

Jednoduchý dotaz na databázi vybere z tabulky clanky článek s ID 6. Většina (začínajících) programátorů je spokojena, dosáhli čeho chtěli a už se nezajímají co dál. Pokud programátor nemá dostatek informací o SQL Injection ani nepřemýšlí jak dotaz ochránit. Byl jsem stejný :-)

V tomto případě předpokládáme, že hodnotu id článku 6 jsme dostali např.
z URL (http://www.vase-domena.cz/index.php?clanek=6).

$id_clanku = $_GET['clanek'];

Pokud hodnotu před použitím neošetříme, útočník může udělat následující. K URL připíše:

http://www.vase-domena.cz/index.php?clanek=6 OR 1=1 --

a SQL dotaz se doplní do následující podoby:

SELECT * FROM clanky WHERE id_clanek = 6 OR 1=1 --

Jelikož 1=1 je vždy pravda, tento SQL dotaz ovlivní celou tabulku (vyhovují všechny řádky). Pokud by byl dotaz doplněn ještě o LIMIT nebo ORDER BY, dvě mínuska -- zajistí, aby zbytek dotazu byl brán jako komentář. V postgreSQL se komentuje právě pomocí --.
Jakmile tento dotaz projde a funguje, může útočník začít s útoky.

Příklad útoku. Vloženou hodnotu doplníme o následující:

SELECT * FROM clanky WHERE id_clanek = 6 OR 1=1; DROP TABLE clanky; --

A máme celou tabulku na onom světě.

TIPy na obranu:
Vytvoříme si php třídu s funkcemi na ošetření vstupních hodnot, která by mohla vypadat následovně. Soubor pojmenuji class.functions.php.

<?php
class kloe_functions {
 
  function __construct() {
 
  }
 
  // fce zkontroluje vstupni hodnotu proti nebezpecnym znakum
  function sqlCheck($test_str) {
    $test_str = strtr($test_str," ","x");
    $test_str = strtr($test_str,"+","x");
    $test_str = strtr($test_str,"--","x");
    $test_str = strtr($test_str,"&","x");
 
    return $test_str;
  }
 
  // funkce zjisti zda je cislo opravdu cislo
  // 444 = true, 43 = true
  // 44a = false, 4 4 = false
  function is_number($number) {
    if (preg_match ("/^([0-9]+)$/", $number)) {
      return 1;
    } else {
      return 0;
    }
  }
 
   // zjisti zda vlozene cislo je kladne cislo v danem rozsahu
  function validate_number_range($number,$min,$max) {
    if (eregi("^[0-9]{".$min.",".$max."}$", $number)) {
      return 1;
    } else {
      return 0;
    }
  }
 
} # konec tridy kloe_functions
?>

Funkci sqlCheck() jsem zahrnul už v php třídě pro připojení k postgreSQL. Funkce zjistí, zda se v řetězci nenachází nebezpečné znaky. Pokud ano, nahradí je znakem x. Funkci používám při přihlašování uživatelů. Ověřuji vložené přihlašovací jméno i heslo.

Fce is_number() se může zdát zbytečná. Vždyť ke zjišťování, jestli je číslo číslem existuje implementovaná fce is_numeric(). Podle mě je is_numeric() nepoužitelná. Zkuste testovat hodnotu 4e4, jestli je číslo ;-) třeba pomocí:

<?php
 
$testovane_cislo = "4e4";
 
if(is_numeric($testovane_cislo)) {
  echo "je cislo";
} else {
  echo "NENI cislo";
}
?>

Při programování zhruba víte, jaké hodnoty dostáváte, takže fce si upravte podle potřeby. Třeba fce validate_number_range() hlídá i rozsah čísla (můžete zabránit přetečení datového typu a opětovné vypsání chyby).

Příklad použití:

<?php
  // pripojeni pripravenych souboru
  include_once("config.inc.php");
  include_once("class.connect.php");
  include_once("class.functions.php");
 
  $kloe = new kloe_functions();
  $db = new db();
  $db->connect();
?>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="cs" lang="cs">
 
<head>
  <title>eKloe.com - fix sql injection</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
 
<?php
 
  $login_name = htmlspecialchars(trim($_POST['login_name']));
  $login_passwd = htmlspecialchars(trim($_POST['login_passwd']));
 
  $login_name = $kloe->sqlCheck($login_name);
  $login_passwd = $kloe->sqlCheck($login_passwd);
 
  // dale by nasledovalo zpracovani promennych
  // .............
 
  // pokud bychom testovali promenou na cislo, priklad by vypadal nasledovne:
 
  if($kloe->is_number($_POST['cislo']) == 1) {
    echo "TESTOVANE CISLO JE OPRAVDU CISLO";
    $db->query("SELECT * FROM clanky WHERE (id_clanku = '".$_POST['cislo']."') LIMIT 1");
    // zde by mohlo byt zpracovani vypisu z databaze
  } else {
    echo "TESTOVANE CISLO NENI CISLO";
    die;
  }
 
?>
 
<br /><br />
<form method="POST" action="index.php">
  Přihlašovací jméno: <input type="text" name="login_name" maxlength="32" />&nbsp;&nbsp;
  Heslo: <input type="password" name="login_passwd" maxlength="32" />&nbsp;&nbsp;&nbsp;
  <input type="submit" value="přihlásit" alt="Přihlásit" />
</form>
 
</body>
</html>

Pár rad na závěr:

V první řadě nevypisujte všechny chybové hlášky uživatelům. Osvědčilo se mi uživatelům podšoupnout pouze chybovou hlášku, se kterou se spokojí (třeba: neočekávaná chyba, kontaktujte prosím webmastera ;-) ) a skutečná errorová hláška se mi pošle na email a do logu. Hned vím, že je něco špatně a vím, kde a kdy.

V postgreSQL se mi velmi vyplatilo používat tzv. VIEWs (nadhledy nad tabulkami). Přímo v databázové vrstvě si můžete vytvořit náhled (jakýkoliv SELECT, JOIN, spožené výrazy, apod.). Tyto náhledy jsou pouze pro čtení a útočník tedy tabulku nemůže poškodit, pokud zná název. Můžete si například vytvořit VIEW uživatelů, ale podmínkou do nich nezapojit administrátorské účty a útočníkovi se v případě prolomení nezobrazí všechny záznamy.

Zvyknětě si v SQL dotazech každou podmínku za WHERE uzavírat do závorek, některým útočníkům to nemusí hned docvaknout. Viz.:

SELECT * FROM user WHERE (id_user = 4 OR 1=1 -- ) ORDER BY id_user LIMIT 1;

Hesla v databázi (ani nikde jinde) neukládejte v původní podobě. Použijte v php hashovací funkce sha1 nebo md5. Útočníkům jde především o hesla a získání přístupů.

Další zdroje:

Článek neberte jako návod na útok (psal bych úplně jiným směrem), ale spíše jako prevenci. Popsané soubory class.connect.php a config.connect.php jsem popsal v článku PHP třída pro připojení k postgreSQL.


Sdílej:
  • Facebook
  • Google Bookmarks
  • Live
  • Twitter
  • StumbleUpon
:,
8 komentářů:
  1. Pavel Stěhule

    Proc, proste, nepouzijete funkci pg_query_params? Toto reseni je absolutne neprustrelne z principu - parametry se udruzuji oddelene od SQL prikazu.

  2. LucaS

    Díky za doplnění… Úplně jsem zapomněl… tato funkce je až od verze php 5.1+, spravuji dost vebů, které ještě přežívají na nižších verzích.

  3. newbie

    stačí použít mysql_real_escape_string() a všechny proměnné uzavírat do úvozovek (’, “, `).
    Funkce je dostupná i na starších verzích PHP a v kombinaci s úvozovkama zabraňuje výše zmíněnému typu SQL injekce.

  4. Gulliver

    Funkce is_numeric funguje dobře …. tedá podle jaké potřeby. Jinak číslo 4e4 je číslo a to hexadecimální (šestnáctková soustava).

    newbie: jen tato funkce “mysql_real_escape_string() ti nepomůže proti sql injection a to ani za použití uvozovek ;)

  5. LucaS

    JJ jasně, ale běžně je potřeba ověřit vstup čísla v desítkové soustavě. Tj. tel číslo, psč apod… is_numeric tyhle soustavy asi nerozlišuje z mezinárodních důvodů. Irsku jsem se běžně setkal s tím, že delší čísla zapisují v šestnáctkové soustavě.

  6. Gulliver

    s tohoto důvodu si dělam na vše vlastní funkce a to i na rč či ičo dič apod vše ma i jiné pravidla než jen čislo ;) a když maš mezinárodní stránku tak je to o to těžší jak ty řikaš např s tim psč … to si pak člověk musi vše nastudovat a zdržuje to v psani kodu
    no nic jsem trochu OT sry

  7. Peli

    Já osobně bych u té funkce is_number použil na výstupu false / true a ne 1 / 0.

  8. LucaS

    To už je na každém, kdo co víc upřednostňuje… U nových funkcí už také dávám true/false …

Odpovědět

Hledat: