Autor Zpráva
Kuba5
Profil
Zdravím, dělám malý skoro soukromý image hosting (jen screeny ze hry) a potřebuji se zeptat zkušenějších na bezpečnost. Potřeboval bych zkontrolovat zdroják, protože mám na VPS vyplý safe mod, takže se v PHP dá dostat kamkoliv a je to dost nebezpečné.

Tady je zdroják (adresy jsem přepsal):
<?php
$koncovky = array('jpg', 'jpeg');

$chyba = "";
if(isset($_POST['nahrat'])){ 
if (!$_FILES || $_FILES["obrazek"]["error"] == UPLOAD_ERR_INI_SIZE) {
    echo("<center><b><span style='color:blue'>Maximální velikost je " . ini_get('upload_max_filesize') . ".</span></b></center>\n");
} elseif ($_FILES["obrazek"]["error"] == UPLOAD_ERR_NO_FILE) {
    echo("<center><b><span style='color:red'>Nevybrali jste soubor, který chcete nahrát.</span></b></center>\n");
} elseif ($_FILES["obrazek"]["error"]) {
    echo("<center><b><span style='color:blue'>Soubor se nepodařilo nahrát, kontaktujte prosím správce serveru.</span></b></center>\n");
} elseif (!in_array(strtolower(pathinfo($_FILES["obrazek"]["name"], PATHINFO_EXTENSION)), $koncovky)) {
    echo("<center><b><span style='color:blue'>Koncovka souboru musí být jedna z: " . implode(", ", $koncovky) . ".</span></b></center>\n");
} elseif (!($imagesize = getimagesize($_FILES["obrazek"]["tmp_name"])) || $imagesize[2] > 3) {
    echo("<center><b><span style='color:blue'>Typ obrázku musí být JPG.</span></b></center>\n");
} else {

$uploaddir = "http://neco.eu/upload/images/";
$name_ciste = $_FILES["obrazek"]["name"];
$name = mt_rand(11111, 99999) . $_FILES["obrazek"]["name"];

    move_uploaded_file($_FILES["obrazek"]["tmp_name"], "upload/$name");
    
    echo "<center><img src='$uploaddir$name' /></center>";
    echo("<center></br>Obrázek " .$name_ciste. " byl nahrán. </br>Tento text vlož do příspěvku:</br>test<br><br><br></center>");
    
}
}
?>

<html> 

<center><form method="post" enctype="multipart/form-data">
    <input type="file" name="obrazek" multiple="multiple" />
    <br><br><input type="submit" name="nahrat" value="Nahrát" />
</form>     

<? /*SCRIPT PRO ZJISTENI VELIKOSTI SLOZKY*/

$dirname='upload';
if ($handle = opendir($dirname)) {
$cv=0;
while (false !== ($file = readdir($handle))) {
if ($file != "." && $file != "..") {
$file="$dirname/$file";
$velikost=filesize ( $file )/1048576;
$cv+=$velikost;
}
}
closedir($handle);
}
echo "<br><br>Velikost nahraných obrázků: ".round($cv,2)." MB";
  /*--------------------------------------*/
?>
  
</center>
</html>

Je tento kód bezpečný? Nejdou uploadnout jiné formáty než .jpg, takže by neměl jít nahrát žádnej script, ale i tak mám strach, protože jak jsem už psal, vše běží na soukromém VPS, takže musím být opatrný.
Díky
ProbablyYes
Profil
Zdravím,

Jestli nechceš, aby někdo nahrál potenciálně nebezpečný soubor - jednoduše přehazuj koncovku na vlastní - např. txt.
Kuba5
Profil
ProbablyYes:
Ok, ale u fotek nic nehrozí ne? Jinak díky.
Virtus
Profil
Zdravím,
Čistě teoreticky, než přehodit koncovku na vlastní je lepší zkontrolovat hlavičky souboru, protože přehození koncovky nemusí pomoct, v momentě kdy si totiž vyžádáž soubor, nezávisle na koncovce, s ním systém pracuje podle hlaviček jaké si soubor nese, takže přesto, že potenciální útočník soubor po nahrátí nebude schopen spustit (protože jej přejmenuješ), spustíš si jej sám až si jej od systému vyžádáš. Pro kontrolu hlaviček jpg sem našel, že se dá v PHP použít funkce http://php.net/manual/en/function.exif-read-data.php, ale zkušenosti s ní nemám.
Jan Tvrdík
Profil
Virtus:
Čistě teoreticky, než přehodit koncovku na vlastní je lepší zkontrolovat hlavičky souboru
Nesmysl, radíš hodně nebezpečné věci. Bezpečnost uploadu stojí téměř vždy primárně na kontrole koncovky, protože podle ní se webový server obvykle rozhoduje, jak soubor zpracuje. Validní obrázek může zároveň obsahovat PHP kód.
Virtus
Profil
Jan Tvrdík:
Ano proto tam je to: Čistě teoreticky.
Možná sem v tomhle směru lechce paranoidní, protože přesně tímto způsobem se mi na server dostali zaškodníci, i když to teda bylo přes nahrané video, od tý doby ratši kontroluju hlavičky a mám svatý klid. A zhruba tak jednou za půl roku narazím na serveru nato, že to někdo takto zkouší, nic méně jedná se o stránky, kde je návštěvnost +- 1M unikátních přástupů za den. Takže s tebou tady souhlasím, že zde je něco takového zbytečné a nebezpečné. Omlouvám se tedy, pokud sem uved někoho v omil a za zbytečné informace ;)
Kuba5
Profil
Díky za rady, u obrázků to asi zatím nechám být.
Ale potřeboval bych poradit, jak zakázat obnovení stránky (F5) po uploadu, dá se tak snadno "spamovat".
Nějaké rady? Třeba timer je dobré řešení, ale potřeboval bych i zakázat to obnovení.
Virtus
Profil
Dle mého názoru je nejlepší použít php header();. Odešlu obrázek, zpracuju obrázek, a pomocí PHP po zpracování reloadnu na nějakou stránku, v tvém případě by to mohla být třeba stránka s nahraným obrázkem.
header("Location: http://www.example.com/");
exit;
Jan Tvrdík
Profil
Kuba5:
Po každém úspěšném POST požadavku by mělo následovat přesměrování.


Virtus:
Možná sem v tomhle směru lechce paranoidní
Na tom není nic paranoidního, to je normální blbost. Obsah souboru klidně zkontrolovat můžeš, ale kontrolu koncovky (za předpokladu normálního prostředí) vynechat prostě nelze. To pomýlím skutečnost, že podle mě, je i zbytek toho příspěvku (ta vysvětlující část) nesmysl, ale třeba je jenom nesmyslně napsaný nebo já nechápavý.
Kuba5
Profil
Přesměrování je nejlepší řešení, díky.
Přes $_GET budu obsah proměnné $name přenášet na novou stránku ($name je název souboru i s ID).
Chro
Profil
Kuba5 [#1]:
Co se týče bezpečnosti uploadu obrázků, je dobré detekovat, zda soubor obsahuje EXIF informace. Pokud ano, tyto informace ze souboru extrahovat a uložit zvlášť (pokud je potřebuješ) a fotografii převzorkovat pomocí GD knihovny - tím se EXIF odstraní.
Pokud ne, soubor zkontrolovat na přítomnost řetězců jako <script, <?php a <?[mezera] a v případě nálezu fotku převzorkovat v GD funkcemi imagecreatefrom... a imagejpeg, imagepng, imagegif.
Varovnou hlášku o možné přítomnosti škodlivého kódu v souboru bych nezobrazoval, běžný uživatel moc nepochopí o co jde, útočníka bych ponechal v blaženém pocitu vítězství, a soubor bych nenápadně zkontroloval a případně ošetřil.

EDIT:
V případě provozování imagehostingu zaměřeného nejen na screeny z her, je odstraňování EXIFu z fotografií priorita č. 1. Uživatel může např. nahrát fotku z domova a neuvědomit si, že obsahuje GPS souřadnice jeho bydliště.
Jan Tvrdík
Profil
Chro:
fotografii převzorkovat pomocí GD knihovny
To je dobrý postup.

soubor zkontrolovat na přítomnost řetězců jako <script, <?php a <?[mezera]
To je blbost, proč by obrázek takové sekvence bytů nemohl obsahovat?
Str4wberry
Profil
Je třeba si uvědomit, že není nebezpečné, když se nebezpečný kód dostane na server — nebezpečné je, až když takový kód jde spustit, čemuž v drtivé většině případů zabrání ta koncovka.
Chro
Profil
Jan Tvrdík [#12]:
Útočník může např. vložit škodlivý PHP kód začínající "<?php" do jpg obrázku třeba do položky Title v EXIFu a nahrát na web přes formulář. Obrázek i tak bude stále nepoškozený a zobrazitelný. Nebo do gifu, byť tím obrázek možná poškodí, ale kdo si toho všimne? Když se pak dostane k nastavení serveru a povolí vykonávání PHP v souborech s příponou jpg nebo gif, vznikne problém.
Kuba5
Profil
No vzhledem k tomu, že EXIF informace nepotřebuju a jsou mi k ničemu, chtěl bych je pomocí GD rovnou v obrázku mazat a obrázek pak hned uploadnout.. může mi někdo poradit s kódem? Nikdy jsem se s GD knihovnou nesetkal a už jsem několik let s PHP nepracoval, takže mám teď často problémy. Potřebuji odstranění EXIF dostat do tohoto kódu:

<?php
$koncovky = array('jpg', 'jpeg');

$chyba = "";
if(isset($_POST['nahrat'])){ 
if (!$_FILES || $_FILES["obrazek"]["error"] == UPLOAD_ERR_INI_SIZE) {
    echo("<center><b><span style='color:blue'>Maximální velikost je " . ini_get('upload_max_filesize') . ".</span></b></center>\n");
} elseif ($_FILES["obrazek"]["error"] == UPLOAD_ERR_NO_FILE) {
    echo("<center><b><span style='color:red'>Nevybrali jste soubor, který chcete nahrát.</span></b></center>\n");
} elseif ($_FILES["obrazek"]["error"]) {
    echo("<center><b><span style='color:blue'>Soubor se nepodařilo nahrát, kontaktujte prosím správce serveru.</span></b></center>\n");
} elseif (!in_array(strtolower(pathinfo($_FILES["obrazek"]["name"], PATHINFO_EXTENSION)), $koncovky)) {
    echo("<center><b><span style='color:blue'>Koncovka souboru musí být jedna z: " . implode(", ", $koncovky) . ".</span></b></center>\n");
} elseif (!($imagesize = getimagesize($_FILES["obrazek"]["tmp_name"])) || $imagesize[2] > 3) {
    echo("<center><b><span style='color:blue'>Typ obrázku musí být JPG.</span></b></center>\n");
} else {

$uploaddir = "http://example.com/test/images/";
$name_ciste = $_FILES["obrazek"]["name"];
$name = mt_rand(11111, 99999) . $_FILES["obrazek"]["name"];              

    move_uploaded_file($_FILES["obrazek"]["tmp_name"], "images/$name");
    
    header("Location: http://www.example.com/test/image.php?id=".$name."");
    exit;
}
}
?>

Děkuji
Chro
Profil
Před header s přesměrováním vlož:
if (exif_read_data("images/$name", 0, TRUE) !== FALSE)
{
$img = imagecreatefromjpeg("images/$name");
imagejpeg($img, "images/$name", 85);
//85 je kvalita obrázku, smysl má zadávat cca 75-90
}
Jan Tvrdík
Profil
Chro:
Když se pak dostane k nastavení serveru…
…tak budeš mít mnohem, mnohem větší problémy, než nějaký upload obrázků. Ke úpravě konfigurace serveru obvykle potřebuješ root práva. A pokud má útočník root práva, tak to je prostě game over =)
Kuba5
Profil
Díky moc. Takže se normálně nahraje obrázek s případným EXIF info, přesune se i s EXIF do složky images (viz move_uploaded_file) a až poté script (ten, který jsi napsal) zkontroluje, zda se v obrázku nachází EXIF kód a pokud ano, tak vytvoří nový obrázek se stejným názvem (akorát už bez EXIF) a přepíše ten původní s EXIF?
Pak můžu normálně zobrazovat obrázek s původním názvem?
Ještě pro jistotu přidám zdroják pro kontrolu..
<?php
$koncovky = array('jpg', 'jpeg');

$chyba = "";
if(isset($_POST['nahrat'])){ 
if (!$_FILES || $_FILES["obrazek"]["error"] == UPLOAD_ERR_INI_SIZE) {
    echo("<center><b><span style='color:blue'>Maximální velikost je " . ini_get('upload_max_filesize') . ".</span></b></center>\n");
} elseif ($_FILES["obrazek"]["error"] == UPLOAD_ERR_NO_FILE) {
    echo("<center><b><span style='color:red'>Nevybrali jste soubor, který chcete nahrát.</span></b></center>\n");
} elseif ($_FILES["obrazek"]["error"]) {
    echo("<center><b><span style='color:blue'>Soubor se nepodařilo nahrát, kontaktujte prosím správce serveru.</span></b></center>\n");
} elseif (!in_array(strtolower(pathinfo($_FILES["obrazek"]["name"], PATHINFO_EXTENSION)), $koncovky)) {
    echo("<center><b><span style='color:blue'>Koncovka souboru musí být jedna z: " . implode(", ", $koncovky) . ".</span></b></center>\n");
} elseif (!($imagesize = getimagesize($_FILES["obrazek"]["tmp_name"])) || $imagesize[2] > 3) {
    echo("<center><b><span style='color:blue'>Typ obrázku musí být JPG.</span></b></center>\n");
} else {

$uploaddir = "http://example.com/test/images/";
$name_ciste = $_FILES["obrazek"]["name"];
$name = mt_rand(11111, 99999) . $_FILES["obrazek"]["name"];              

    move_uploaded_file($_FILES["obrazek"]["tmp_name"], "images/$name");
    
    if (exif_read_data("images/$name", 0, TRUE) !== FALSE)
    {
    $img = imagecreatefromjpeg("images/$name");
    imagejpeg($img, "images/$name", 85);
    }

    header("Location: http://www.example.com/test/image.php?id=".$name."");
    exit;
}
}
?>

Stránka, kde se zobrazí uploadnutý obrázek i s odkazem vypadá asi takto:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>

<?php

if(isset($_GET["id"]))
{
$uploaddir = "test/images/";
$name = $_GET["id"];

echo "<center><img src='$uploaddir$name' /></center>";
echo("<center></br>Obrázek " .$name. " byl nahrán. </br>Tento text vlož do příspěvku:</br>test<br><br><br></center>");
}

?>

</body>
</html>

Nicméně po uploadu mám tento warning:
Warning: Cannot modify header information - headers already sent by (output started at /var/www/upload/test/index.php:7) in /var/www/upload/test/index.php on line 35

Fotka se nahraje, ale bohužel se logicky neprovede přesměrování.
Jan Tvrdík
Profil
Nejčastější potíže s PHP (FAQ) » headers already sent…
Kuba5
Profil
Ok, vyřešeno. Myslel jsem, že bude muset jít ven echo, ale problém byl HTML kód před <?php.


Ještě bych se chtěl zeptat, jak je to teda s EXIF.. (víc horní příspěvek se zdrojákama)
Konkrétně, co provede ten script od "Chro".
Díky
Dusann
Profil
Chro:
imagejpeg($img, "images/$name", 85); //85 je kvalita obrázku, smysl má zadávat cca 75-90

GD nie je na znovu-ukladanie JPEG ideálne, pretože nepoznáš hodnotu kompresie z pôvodného obrázku. Skončíš teda buď s obrázkom ktorého veľkosť súboru je väčšia a bez pridanej (ak nie horšej) kvality, alebo zbytočne degraduješ kvalitu pôvodného obrázku viac ako je nutné. Musel by si binárnou cestou extrahovať z metadát údaj o JPEG kompresii pôvodného JPG a použiť ho pri znovu-uložení aby si dosiahol minimálnu stratu kvality obrázku + zachoval približne pôvodnú veľkosť súboru.

Pre znovu-uloženie JPEG je preto lepšie použiť Imagick v ktorom vieš jednoducho získať hodnotu JPEG kompresie z načítaného image resource.

Inak najideálnejšie by samozrejme bolo stripovať ASCII metadáta z obrázku a nie kvôli tomu znovu-ukladať obrázok druhý krát, keďže vzniká zbytočná rekompresia. Nepoznám však žiadne riešenie pre PHP ktoré by stripovalo textové metadáta z obrázku bez toho, aby došlo k znovu-uloženiu. Jedine ak by bol nejaký expert ktorý by bol schopný vytvoriť vlastnú funkciu, ktorá by toto riešila binárnou úpravou súboru.

Každopádne, pokiaľ sa pri znovu-uložení JPEG použije rovnaká hodnota kompresie, dôjde k minimálnej strate kvality, ktorú na prvý pohľad snáď ani nie je možné rozoznať (pokiaľ použitý JPEG enkoder pracuje podobne kvalitne ako enkoder pri prvom uložení ), takže riešenie jedného znovu-uloženia JPEG obrázku je myslím OK. Znovu-ukladanie som v Imagicku testoval a nespozoroval som pri rôznych obrázkoch žiadne badateľné degradácie ak som použil rovnakú hodnotu kompresie ako mal obrázok pôvodne.
Kuba5
Profil
No nejde mi až tak o kvalitu, budou tam asi jen screeny z jedné hry a ta nejspíš žádné EXIF informace nemá, takže se nemá co znovu uložit.
Mně jde hlavně o bezpečnost, už jsem uvažoval, že ten uploader hodím na nějaký free webhosting, ale zase nechci riskovat smazání poskytovatelem, i když screeny bych mohl celkem jednoduše přes cron zálohovat právě na to VPS, akorát bych musel znemožnit spuštění scriptu, kdyby se přeci jen nějaký v nějakém obrázku našel.


Jak už jsem říkal, uploader běží na VPS, kde je zablokovaný safe mode a při hacknutí se útočník okamžitě dostane k rootu a to je můj konec.
Jan Tvrdík
Profil
Kuba5:
akorát bych musel znemožnit spuštění scriptu, kdyby se přeci jen nějaký v nějakém obrázku našel.
Ještě jednou, kdyby se to ztratilo v těch bludech, pokud zkontroluješ příponu souboru (což tebou uvedený skript dělá dobře), tak je ten skript bezpečný. PHP kód v obrázcích se správnou příponou nepředstavuje pro server bezpečnostní riziko.

kde je zablokovaný safe mode
safe_mode je blbost, od PHP 5.4 je úplně zrušený. Jediné, co dává trochu smysl, je open_basedir.

Vaše odpověď

Mohlo by se hodit


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm: