Hlavní navigace

PHP pro experty: bezpečnost

12. 8. 2004
Doba čtení: 8 minut

Sdílet

Programujete v PHP bezpečně a efektivně? Nebo považujete PHP za nedokonalý jazyk, který je nebezpečný již z principu? Pokud se chcete přesvědčit o tom, že v PHP lze programovat čistě a bezpečně, čtěte dále.

Úvod

K napsání tohoto článku/seriálu mě vedl jeden hlavní důvod, zkušenost s různými programátory v PHP. Ačkoliv jsem kdysi (bohužel) mylně předpokládal, že programovat v PHP umí každý druhý, ani jsem netušil, jak jsem se mýlil. PHP je totiž jazyk, který vychází z Perlu, čímž si z něj odnáší jak výhody, tak i nevýhody. A tou největší nevýhodou PHP je to, že vám umožní dělat věci, které jsou nejenom problematické a neefektivní, ale taktéž velmi nebezpečné.

Právě díky těmto vlastnostem PHP odsuzuje mnoho profesionálních vývojářů, kteří si myslí, že v PHP nelze programovat čistě a bezproblémově. Pro všechny tyto vývojáře i jiné bych rád připravil sérii článků, jež se bude do podrobnosti zaobírat těmi nejzáludnějšími věcmi a funkcemi jazyka. Jež vám ukáže možnosti, jak věci řešit elegantně a správně bez zbytečných problémů.

Bezpečné používání include/require

Nebezpečné a neošetřené vkládání souborů do skriptů v PHP není doménou jen naprostých laiků, ale bohužel i pokročilejších programátorů. Zde ukážu názornou a velmi známou ukázku chybně napsaného šablonovacího systému v PHP:

include "header.php";
include $_GET['page'];
include "footer.php"; 

Předešlý příklad je relativně dobře známý a obsahuje až příliš mnoho zřejmých bezpečnostních problémů. Pokud například zadáme adresu skript.php?page=/etc/passwd, skript nám může vypsat soubor se jmény a hesly uživatelů (samozřejmě pouze pokud PHP neběží v tzv. safe mode a má příslušná oprávnění).

Uvedených chyb si všimnou většinou pokročilejší uživatelé PHP, ale profesionálové najdou chyb mnohem více. Například použití dvojitých uvozovek není nutné, a tím se zbytečně zvyšuje hardwarová náročnost aplikace. Místo dvojitých uvozovek bychom v případě, že vkládáme nijak neformátovaný text, měli zásadně používat uvozovky jednoduché. Dalším, tentokrát již bezpečnostním problémem je použití nesprávné funkce pro vkládání obsahu souboru. Podle mých zkušeností jen málo programátorů v PHP zná rozdíl mezi funkcemi require a include. Jak si sami můžete v dokumentaci k PHP přečíst, funkce require při neúspěchu vrací závažnou (fatal) chybu, jež ukončí běh skriptu, na rozdíl od funkce include jež hlásí pouze varování. Pokud je tedy soubor pro běh skriptu nezbytný, zásadně byste měli používat funkci require.

Ale přestaňme se točit okolo „horké kaše“ a podívejme se na zásadní změny ve skriptu, které bychom měli provést.

  1. Ošetřit vstup $_GET['page'] před vložením zákařného kódu
  2. Ošetřit vstup $_GET['page'] před vložením neexistující stránky
  3. Opravit drobné kódovací chyby

1. Ošetření vstupu před zadáním nepovolených znaků

Pro ošetření vstupu před nepovolenými znaky se velmi často používají regulární výrazy. Jejich výhoda tkví v naprosté univerzálnosti použití pro ošetření libovolného vstupu. Zde si ukážeme malý příklad správného ošetření vstupu:

$mypage=eregi_replace('[^0-9a-z\-\_]', '', $_GET['page']; 

tip: Pokud vám stačí vkládat jako parametr číslo, doporučuji místo složitých regulárních výrazů používat jednoduché přetypování jako $mypage = (int) $_GET['page']; , které zajistí, že proměnná $mypage bude vždy obsahovat pouze číslo

Jak vidíte, výše vstup ošetřuji tak, že jakékoliv nealfanumerické znaky odstraním. Abych ovšem mohl do skriptu vložit jiný soubor, je ještě třeba znát nejenom jeho jméno, ale i plnou cestu k němu. Používání relativních odkazů nedoporučuji.

$mypage=eregi_replace('[^0-9a-z\-\_]', '', $_GET['page']; $mypage=dirname(__FILE__) . DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . $mypage . '.html';

Předešlý kód ukazuje správné ošetření získání jména souboru ke stránce ./pages/home.html. Pokud se například podíváte pozorněji, zjistíte, že místo lomítka používám konstantu DIRECTORY_SEPARATOR, která obstarává přechod mezi systémy unix a win32, kde se používají odlišná lomítka. Budete se pravděpodobně velice divit, ale mnoho programátorů právě na tomto místě zbytečně chybuje.

2. Výsledný skript

A zase vracíme k obvyklé chybě mnoha programátorů – při vkládání dat od uživatele nestačí jen ověřit to, jestli neobsahují nebezpečný kód, ale i to, zda jsou pravdivá. Za největší hloupost považuji spoléhání se na to, že vyžadovaná stránka existuje. Pokud tomu totiž tak není, skript jistě vrátí nějakou chybovou hlášku (popřípadě dojde k zastavení jeho vykonávání), což koliduje s tím, co by mělo nastat. Jestliže stránka neexistuje, měli byste přinejmenším odkázat na úvodní stránku, popřípadě zaslat uživateli hlášení 404: Not Found. Jednoduché ověření existence stránky a také konečný a funkční kód vidíte níže:

require 'header.php';
$mypage=eregi_replace('[^0-9a-z\-\_]', '', $_GET['page'];
$mypage=dirname(__FILE__) . DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . $mypage . '.html';
if(file_exists($mypage)){
  require $mypage;
} else {
  // error_log("This file doesn't exist: $mypage\n", 3, FILE_ERROR);  // případné zaznamenání chyb
  require dirname(__FILE__) . DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . 'index.html';
}
require 'footer.php'; 

3. Co si zapamatujte

  • vždy ověřujte všechny vstupy od uživatele
  • místo include raději používejte require, vyhnete se tak nepředvídatelným situacím, kdy se skript chová v případě neexistence souboru jinak, než bylo požadováno
  • neúspěšné pokusy o nalezení souboru zaznamenávejte do logu
  • pokud přijímáte od uživatele číslo, vždy jej přetypujte  (int) $cislo
  • místo lomítek používejte konstantu DIRECTORY_SEPARATOR
  • tam, kde nejsou potřeba dvojité uvozovky, používejte jednoduché

Přetypování

Jak už jsem se výše zmínil, nejsnadnějším způsobem při vstupu čísla je přetypování proměnné na typ int. Ovšem neupozornil jsem vás na potenciální problém, který může přetypování způsobit, podívejte se na skript níže:

$cislo = (int) $_GET['cislo'];
if($cislo == 'ahoj'){
echo 'ok';
} 

I když je to zdánlivě nelogické, skript vypíše text „ok“, protože řetězec ‚ahoj‘ se před porovnáním přetypuje také na číslo, tedy 0 == 0, a to platí. Dejte si tedy pozor na to, že v podmínkách se při porovnání řetězce a čísla (int) automaticky konvertuje řetězec a nikoli číslo.

Další informace naleznete na těchto stránkách – www.php.net/lan­guage.types

Vkládání SQL kódu

Při dotazech SQL bývá častým problémem fakt, že do dotazů jsou dávána data přímo od uživatelů. Ukážu vám následující jednoduchý příklad opravdu velmi špatně položeného SQL dotazu:

"SELECT $_GET['what'] FROM users WHERE nick = '$_GET['nick']';" 

Pokud zavoláme tento odkaz:

skript.php?what=name&id=nick 

dostaneme jako výstup jméno uživatele podle jeho přezdívky. Nastává tu ovšem otázka, co se stane, pokud skript zavoláme takto:

skript.php?what=password&id=nick
skript.php?what=name&id=nick`; DROP users; 

První příklad vám ve všech databázích vypíše heslo uživatele, druhý příklad vám umožní zahodit celou tabulku uživatelů. Pro ochranu vstupu dat od uživatelů byste měli minimálně vždy pevně formulovat data, která má databáze vrátit (umožnit uživatelům vybrat sloupec tabulky, která obsahuje hesla, například není příliš moudré) a všechny vstupy ošetřovat proti uvozovkám, středníkům apod. Prvnímu příkladu nelze nijak obecně předcházet, jedinou obranou je dobrá kontrola kódu a inteligence programátora, u druhého příkladu je možné dovolit vkládat do databáze pouze alfanumerické znaky, například takto:

function sql($thing) {  if (is_array($thing)) {   $escaped = array();   foreach ($thing as $key => $value) {     $escaped[$key] = $this->sql($value);   }   return $escaped;  }  return mysql_real_escape_string($thing); }

Předchozí příklad komplexně zpracuje pole se všemi předanými výrazy a umožní jej vložit přímo do SQL. Například můžete použít tento fígl: $dbdata = sql($_POST);

Další informace naleznete na těchto stránkách – www.php.net/mys­ql_escape_string

Kradení výsledků SQL

Tato technika není moc známá ani moc rozšířená, ale přesto by stálo za to o ní alespoň trochu pohovořit. Pokud používáte tento typ dotazů:

SELECT * FROM users 

Vystavujete se riziku, že uživateli může později z nějaké proměnné získat nejenom jména uživatelů, ale i jejich hesla. Proto byste neměli používat vícenásobné výběry, ale pouze přesné dotazy, například:

SELECT username FROM users 

Tímto kódem nejenom zvýšíte bezpečnost vašeho skriptu, ale i zvýšíte rychlost jeho provádění, protože z databáze vybíráte pouze ta data, jež jsou pro běh skriptu nezbytná.

Dobré zvyky při úpravách v SQL

Při testování mého PHP kódu se i dříve stávalo, že jsem omylem vymazal celou tabulku údajů. Není to ostatně ani příliš obtížné, stačí jen špatně definovat podmínku, a je smazáno vše, ostatně podívejte se na krásný příklad níže:

DELETE FROM users; 

Tento dotaz vám smaže všechny záznamy v tabulce users pouze díky tomu, že jste například vaší funkci zapomněli předat podmínku. Není to nic zvláštního, a ač se to teď nezdá, takovéto chyby v kódu nastávají. Proto vždy, pokud mažete jeden řádek, se limitujte na jeden řádek, například takto

DELETE FROM users LIMIT 1; 

Takto ani při špatně postaveném SQL dotazu nemůžete ztratit všechna data. Je více než na pováženou limitovat veškeré SQL dotazy nějakým omezením, a to i v případech, kdy provádíte například příkaz SELECT či UPDATE. Mnohdy byste totiž jinak mohli svou neopatrností způsobit zbytečně velké škody.

Zamaskování PHP

Velmi oblíbenou a také účinnou metodou je zamaskování jména skriptů i jeho proměnných v URL pomocí modulu serveru Apache – mod_rewrite. Změnu přípony souborů z .php  na .html provedeme přidáním těchto řádků do souboru httpd.conf.

LoadModule rewrite_module modules/mod_rewrite.so
Rewriteengine on
Rewriterule (.+).html$ $1.php 

Jak jste si všimli, modul mod_rewrite nahrazuje žádané URL pomocí regulárních výrazů. Zde uvedený příklad je pouze minimální ukázkou možností tohoto velmi silného nástroje, o němž se v dalších dílech určitě zmíníme podrobněji.

Samozřejmě existuje i druhá možnost, a to přiřazení možnosti vykonávání souborům .html, například takto:

AddType application/x-httpd-php .phtml .php3 .php .html 

Tato možnost má však zásadní nevýhodu v tom, že obvykle platí pro celý server a znepřehledňuje kompletně organizaci skriptů. Není pak totiž možné určit, které soubory jsou statické, a které dynamické.

Cloud 24 - tip 1

Další informace naleznete na těchto stránkách – www.php.net/se­curity.hiding

Co vás čeká příště

Pokud se vám tento seriál po technické stránce zamlouvá, můžete čekat jeho pokračování – ostatně o jeho kvalitách se můžete vyjádřit v diskusním fóru. V pokračování bych měl zájem podívat se na administraci webových aplikací pomocí zrychleného vývoje aplikací. Bavit bychom se měli především o napojení na produkty MS Access či Dadabik, které jsou k tomuto účelu nejvhodnější.

Byl pro vás článek přínosný?

Autor článku

V oboru informačních technologií se pohybuje přes 20 let. V současné době pracuje jako kontraktor pro DevOps.