Biztonságos fájlfeltöltés PHP-val

A PHP-val való publikus fájlfeltöltésnél a biztonságra ügyelni kell. Vannak olyan gyakran használt ellenőrző módszerek, amik nem állják meg a helyüket, de egyszerűen javíthatóak.

Először is néhány tévhit az ellenőrzésekről:

  1. Elég egy rejtett MAX_FILE_SIZE mező, hogy a méretet beállítsam.
  2. A MIME-típusban megbízhatok
  3. Csak képeket akarok feltölteni, elég a getimagesize()

Miért nem igazak?

  1. A mezőt csak a böngésző veszi figyelembe – már ha figyelembe veszi. És mivel kliens oldalról jön, így nem lehet megbízni benne. Kényelmi szolgáltatásként használható (de nem kötelező), nehogy a felhasználó fél óráig várjon a feltöltésre, hogy aztán kiírja a program, hogy túl nagy a fájl.
  2. A böngésző küldi, azaz nem megbízható. Firefoxban a társítások a mimeTypes.rdf fájlban vannak. Olvasmány: a Mozilla súgója (angolul), a sebezhetőség bemutatása, leírása (magyarul)
  3. A JPEG fájlok metaadataiban el lehet rejteni sok mindent, így PHP kódokat is. Olvasmány: példák a használatára (angolul)
    A lenti képen pedig egy GIF-be egyázott PHP látható. (Forrás)
    PHP fájl gif fájlban

Ha ez ijesztőnek látszik, akkor jó. Hogyan lehet javítani?

  1. Szerver oldali fájltípus ellenőrzéssel
  2. Fájlkiterjesztés ellenőrzéssel
  3. Szintén fájlkiterjesztésellenőrzéssel: habár a képben ottmarad a PHP fájl, de mivel a kiterjesztése nem PHP, így nem okozhat kárt. Vagy metaadat nélküli formátumra konvertálással (lehetséges módszer angolul).

Építsük fel a biztonságos ellenőrzést!

Először a fontos adatokat a könnyebb hivatkozás érdekében változókba rakjuk és létrehozzuk a megengedett kiterjesztések tömbjét. A kulcs a kiterjesztés lesz, az érték pedig az igaz (true). A példában jpg, png, gif és txt engedélyezett.

1
2
3
4
5
6
7
8
<?php
$allowedtypes=array("jpg"=>true,"png"=>true,"gif"=>true,"txt"=>true);
$filename = $_FILES['file1']['name'];  //fájlnév
$source = $_FILES['file1']['tmp_name'];  //forrásfájl
$file_size=filesize($source); //fájlméret
$saveloc = "uploads/" . $filename; //mentési hely
$nameext=explode(".",$filename); //fájlnévből és kiterjesztésből álló tömb
$maxfilesize=10485760; //max jálméret bájtban (itt a példában 10 megabájt)

Most kezdődik az ellenőrzés. Először ellenőrizzük, hogy a fájlnév megengedett-e: nincs többszörös kiterjesztése (egy darab pont van benne), a kiterjesztés 0-4 karakter közti angol alfanumerikus, a fájlnév pedig legalább 1 karakteres angol alfanumerikus.
Ehhez a következő reguláris kifejezést használjuk:

1
2
3
if(preg_match('/^[A-Za-z0-9\-\_]{1,}\.[a-zA-Z0-9]{0,4}$/',$filename)){
//Továbbiak
}

Most a fájlkiterjesztésre ellenőrzünk: ha szerepel az engedélyezett típusok tömbben és true (igaz) értéke van, továbbmehet:

1
2
3
if(!empty($allowedtypes[strtolower($nameext[1])]) && $allowedtypes[strtolower($nameext[1])]===true){
//Továbbiak
}

Majd a fájlméretet ellenőrizzük:

1
2
3
if($file_size<=$maxfilesize){
//Továbbiak
}

Utolsó ellenőrzésünk pedig: ha nincs már ilyen nevű fájl, akkor feltöltünk (és a chmodot 644-re állítjuk, mert végrehajtani nem kell a fájlokat, “külsősöknek” pedig írási jog sem kell.

1
2
3
4
5
6
7
8
9
if(!file_exists($saveloc)){
//megpróbáljuk mozgatni a fájlt, ha baj van, azért figyelembe vesszük.
if(move_uploaded_file($source, $saveloc)) { 
chmod($saveloc,644);
echo "Sikeres feltöltés. <a href='".$saveloc."'>Fájl megtekintése</a>";
}
else echo "Sikertelen mozgatás";
}
}

Végül minden hiba esetére készítünk egyszerű hibaüzeneteket. Ezután a kész kód (a form-mal együtt):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
if(isset($_POST["submit"])){
$allowedtypes=array("jpg"=>true,"png"=>true,"gif"=>true,"txt"=>true);
$filename = $_FILES['file1']['name'];  
$source = $_FILES['file1']['tmp_name'];  
$file_size=$_FILES['file1']['size'];
$saveloc = "uploads/" . $filename;
$maxfilesize=1024*1024*10;
$nameext=explode(".",$filename);
if(preg_match('/^[A-Za-z0-9\-\_]{1,}\.[a-zA-Z0-9]{0,4}$/',$filename)){
if(!empty($allowedtypes[strtolower($nameext[1])]) && $allowedtypes[strtolower($nameext[1])]===true){
if($file_size<=$maxfilesize){
if(!file_exists($saveloc)){
if(move_uploaded_file($source, $saveloc)) { 
chmod($saveloc,644);
echo "Sikeres feltöltés. <a href='".$saveloc."'>Fájl megtekintése</a>";
}
else echo "Sikertelen mozgatás";
}
else echo "A fájl már létezik.";
}
else echo "A fájl túl nagy.";
}
else echo "A fájl kiterjesztése nem megengedett.";
}
else echo "A fájl neve csak alfanumerikus karaktereket és EGY darab pontot tartalmazhat.";
}
else echo "<form method='post' enctype='multipart/form-data' action='secureupload.php'> Fájl: <input type='file' name='file1' /><br /><input
name='MAX_FILE_SIZE' type='hidden' value='10485760' /> <input type='submit' value='Upload' name='submit' /></form>";
?>

5 HOZZÁSZÓLÁS

  1. Kiegészítés + javaslat:

    Javítás 1. pontja szerver oldali fájltípus ellenőrzéssel. Helyes és jó gondolat, csak kimaradt, mert fájl elnevezést ellenőriztél valamint kiterjesztést, amit még mindig a felhasználó küldött be… Bármikor beküldhetek ilyen esetben egy .php fájlt amit átneveztem .jpg-re, és letárolod, mert lesz mérete, elnevezése megfelel, és a kiterjesztése is allowed, igaz a profil oldalamon nem lesz látható a kép, de jelenleg nem is az a célom…

    Ezért kellene szerver oldalon ténylegesen típust ellenőrizni! Az amit a böngésző átad, az tényleg felesleges, de ezt te is elvégezheted a szerveren:
    http://us3.php.net/manual/en/function.finfo-file.php (igaz nem alap csomag, de létezik és hasznos!)

    A másik amit észre vettem, hogy idézem: “habár a képben ottmarad a PHP fájl, de mivel a kiterjesztése nem PHP, így nem okozhat kárt”.

    Na ez nem igaz! Ilyen esetben is bármikor el lehet érni, feltéve ha találsz arra módot, hogy lefuttasd. Mégis hogyan? Hát ha akár csak egy include() vagy require() esetleg !!eval()!! (stb…) van a honlapodban ami paramétertől függ, akkor ha az nincs megfelelően védve betölthető az állomány és a benne található kód lefuttatható… Igaz, hogy ez már advanced módszer és nem a kis pistike fog ezzel próbálkozni. és itt vissza is tértünk, hogy a 644 sem fog sokat segíteni hiszen olvasási jog bőven elég, hogy fusson egy php kód :)

    Ennek a kijavítására javasolnám, hogy ha képeket fogadsz el, akkor minden extra adatot (pl. EXIF) távolíts el mielőtt lemented az állományt.
    http://www.php.net/manual/en/book.exif.php

    Remélem segítettem, hogy egy kicsit átgondoljuk a biztonság fogalmát.

  2. Ez igaz, de én feltételeztem, hogy user által feltöltött fájlokat include/require/stb. móddal meghívni NEM engedünk. De ha igen, akkor igazak az említettek.

  3. Én kipróbáltam a kódot. Localhoston jól működik viszont mikor felraktam egy ingyenes tárhelyre pl uw akkor sem ír ki semmilyen hibát a file meg is jelenik az uploads mappában viszont (képet töltöttem fel) nem tudom megnézni, sem használni.
    Valaki nem tudja mi lehet a gond? Köszi előre is :)

  4. atw-n nem csinál semmit. Kell valamit szerver adminban bekapcsolni? uploads mappát létrehoztam, nem ír semmi hibaüzenetet, csak nem tölt fel semmit.

  5. Sziasztok,

    Ebben a formában a fájl kiterjesztés ellenörzése problémás lehet:

    $nameext=explode(“.”,$filename);
    $nameext[1]; -> nem biztos hogy a fájl kiterjesztés lesz benne

    Gondoljatok bele mi van ha a fájl név így néz ki: ez.egy.kep.jpg ?
    Javaslom inkább az alábbi használatát:

    $nameext=explode(“.”,$filename);
    $nameext=array_pop($nameext);

    Üdv

HOZZÁSZÓLOK A CIKKHEZ

Kérjük, írja be véleményét!
írja be ide nevét