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?

9. 3. 2005 16:42

benzin (neregistrovaný)
Dost prapodivna debata. Sam jsem tedka podobny problem resil. Oznacovat texty ciselnym ID je mozne na malem webu jinak se v tom casem stratite. Vetsina aplikaci pouziva alespon kretke retezce.

Ja jsem ale radsi sahnul k predkompilovanym jazykovym mutacim. Mam pro kazdy jazyk samostatny soubor prekladu, v trochu hard kor podobe (jako $pole['text skriptu']='text prekladu') tento text staci jen natahnout, spustit pres eval() a mate nadherne naplnene pole. Pak jen spustite skriptik …

15. 1. 2004 13:24

razor (neregistrovaný)

Me ten článek připadal zajimavý :)

Podnikatel.cz: K EET. Štamgast už peníze na stole nenechá

K EET. Štamgast už peníze na stole nenechá

Vitalia.cz: Jmenuje se Janina a žije bez cukru

Jmenuje se Janina a žije bez cukru

DigiZone.cz: TV Philips a Android verze 6.0

TV Philips a Android verze 6.0

Vitalia.cz: Taky věříte na pravidlo 5 sekund?

Taky věříte na pravidlo 5 sekund?

Podnikatel.cz: Změny v cestovních náhradách 2017

Změny v cestovních náhradách 2017

Podnikatel.cz: Udávání a účtenková loterie, hloupá komedie

Udávání a účtenková loterie, hloupá komedie

Podnikatel.cz: Chaos u EET pokračuje. Jsou tu další návrhy

Chaos u EET pokračuje. Jsou tu další návrhy

Měšec.cz: Kdy vám stát dá na stěhování 50 000 Kč?

Kdy vám stát dá na stěhování 50 000 Kč?

Vitalia.cz: Potvrzeno: Pobyt v lese je skvělý na imunitu

Potvrzeno: Pobyt v lese je skvělý na imunitu

Podnikatel.cz: Podnikatelům dorazí varování od BSA

Podnikatelům dorazí varování od BSA

Vitalia.cz: Když přijdete o oko, přijdete na rok o řidičák

Když přijdete o oko, přijdete na rok o řidičák

Měšec.cz: mBank cenzuruje, zrušila mFórum

mBank cenzuruje, zrušila mFórum

Vitalia.cz: Spor o mortadelu: podle Lidlu falšovaná nebyla

Spor o mortadelu: podle Lidlu falšovaná nebyla

Měšec.cz: Air Bank zruší TOP3 garanci a zdražuje kurzy

Air Bank zruší TOP3 garanci a zdražuje kurzy

Lupa.cz: Propustili je z Avastu, už po nich sahá ESET

Propustili je z Avastu, už po nich sahá ESET

Lupa.cz: Proč firmy málo chrání data? Chovají se logicky

Proč firmy málo chrání data? Chovají se logicky

DigiZone.cz: Rádio Šlágr má licenci pro digi vysílání

Rádio Šlágr má licenci pro digi vysílání

Lupa.cz: Insolvenční řízení kvůli cookies? Vítejte v ČR

Insolvenční řízení kvůli cookies? Vítejte v ČR

Podnikatel.cz: Chtějte údaje k dani z nemovitostí do mailu

Chtějte údaje k dani z nemovitostí do mailu

Vitalia.cz: Proč vás každý zubař posílá na dentální hygienu

Proč vás každý zubař posílá na dentální hygienu