Hlavní navigace

(Ne)bezpečí databázových aplikací v PHP

Michal Burda

Jeden řádek kódu, a co dokáže udělat za paseku! Modelový příklad potvrzující, že kontrolu uživatelského vstupu nelze brát na lehkou váhu. A v Internetu hemžícím se nekalými živly to platí dvojnásob, NE, strašně-moc-násob.

K napsání tohoto článku mě vedlo shlédnutí jednoho příšerného kusu kódu, který jeden člověk na jednom nejmenovaném serveru prezentoval jako ukázku řešení jistého problému. Nic proti autorovi, zřejmě to myslel dobře a třeba si i byl vědom bezpečnostní díry, která by nasazením kódu v reálném projektu vznikla. Nicméně se mi zdá nepatřičné začínajícím programátorům v PHP namlouvat, že „takhle se to skutečně dělá“. O co vlastně šlo?

Příklad ukazoval, jak s pomocí databáze řešit vícejazyčnost webovské aplikace. Řekněme v SŘBD MySQL se vytvořila tabulka s nějakým číselným ID zprávy a textovými sloupci langcz pro českou, langen pro anglickou a třeba langde pro německou verzi hlášení:

CREATE DATABASE pokus;
USE pokus;

CREATE TABLE msg (
  id INT NOT NULL AUTO_INCREMENT,
  langcz TEXT,
  langen TEXT,
  langde TEXT,
  PRIMARY KEY(id)
);

INSERT INTO msg (langcz, langen, langde)
  VALUES ("Dobrý den!", "Hello!", "Guten Tag!");


Vtip řešení vícejazyčných textů v aplikaci spočíval ve vytvoření speciální funkce, řekněme msg(), která se starala o vypisování hlášení z databáze v patřičném jazyku. Jako první parametr se jí předávalo ID hlášení a jako druhý zvolený jazyk. Kód aplikace (řekněme nějaký soubor index.php) pak vypadal zhruba takto:

<?
// funkce pro výpis vícejazyčných hlášení
function msg($id, $lang) {
  $result = mysql_query("SELECT lang$lang
                           FROM msg
                           WHERE id=$id");
  if ($result) {
    $row = mysql_fetch_array($result);
    mysql_free_result($result);
    echo $row[0];
  }
}

// připojení do databáze
mysql_connect("localhost", "michal", "heslo");
mysql_select_db("pokus");

// zjištění request proměnné "lang"
$lang = $_REQUEST['lang'];
if (!$lang)
  $lang = 'cz';

// použití vícejazyčné hlášky
msg(1, $lang);

// atd...
?>

Zkusíte-li si uvedený příklad doma a v prohlížeči se podíváte na výsledek skriptu index.php, mělo by se vám objevit hlášení:

Dobrý den!

Použijete-li jako URL řekněme řetězec index.php?lang=en, zobrazí se naprosto správně english message:

Hello!

Achillova pata uvedeného kódu je samozřejmě v proměnné lang. Kdo vám zaručí, že se skript index.php bude volat výhradně s hodnotami „cz“, „en“ nebo „de“ request proměnné lang? Nečiní nejmenší problém do proměnné lang dostat jakýkoliv řetězec, který se naprosto nedotčen propašuje až do SQL dotazu.

Představme si modelovou situaci, totiž že kromě tabulky msg jsou v databázi pokus také jiná data. Třeba tabulka s loginy a hesly uživatelů naší webovské aplikace:

CREATE TABLE user (
  id INT NOT NULL AUTO_INCREMENT,
  name VARCHAR(50),
  login VARCHAR(20),
  password VARCHAR(20),
  PRIMARY KEY(id)
);

INSERT INTO user (name, login, password)
  VALUES ("Rumcajs z Řáholce", "rumcajs", "manka");

(Jako správní zvědavci hesla neukládáme v kódovaném tvaru.) Teď už je malér na spadnutí. Stačí se trochu zamyslet a jako správný škaredý hoch/dívka formulovat následující URL pro náš děravý skript:

index.php?lang=.name%20FROM%20user%20lang%20WHERE%20id=1%20/*

Aplikace nám poslušně odpoví:

rumcajs

Vida, login prvního uživatele. Zadáme-li pro změnu něco takového:

index.php?lang=.password%20FROM%20user%20lang%20WHERE%20id=1%20/*

skriptík promptně dodá i heslo:

manka

Co přesně se stalo? V případě druhého URL se do proměnné lang uložil řetězec .password FROM user lang WHERE id=1 /*. Ve funkci msg() se tedy provedl následující SQL dotaz (pro lepší přehlednost jsem jej pouze rozdělil na více řádků):

SELECT lang.password
  FROM user lang
  WHERE id=1
/*FROM msg WHERE id=1

Praví se v něm, že chceme vrátit hodnotu atributu password relace lang z tabulky user, kterou si vtipně nazveme lang, abychom nějak „vybruslili“ se zapeklitou předponou z původního SQL dotazu. Žádáme pouze záznam s id rovným jedné. Dvojice znaků „/*“ na konci řetězce v proměnné lang zajistí, že zbytek původní části SQL dotazu bude chápán jako komentář.

A je vymalováno. Průměrnému programátorovi už nebude dělat problém napsat si skriptík, který bude v cyklu generovat URL, v nich zkoušet všechna čísla id a z výsledků „vykusovat“ loginy a hesla uživatelů.

Lék je přitom tak jednoduchý. Bohatě stačí v kódu trochu změnit podmínku konstrukce if:

if (!in_array($lang, array('en', 'de', 'cz')))
  $lang = 'cz';

Jaké z toho plyne poučení? Když se v manuálu PHP píše: „nevěřte datům přicházejícím od uživatele“, nezbývá než poslechnout a důsledně kontrolovat, přetypovávat a zajišťovat uživatelské vstupy. V tomto případě jsme získali loginy a hesla všech uživatelů. Znásilněním jiných typů SQL dotazů (UPDATE, INSERT, DELETE) můžeme docílit dalších škod. (O uživatelských vstupech a funkci eval() raději ani nemluvím… ;-)

Našli jste v článku chybu?