Stažení souboru po ověření práv
Školení, která pořádám
Některé soubory můžeme chtít zpřístupnit jen přihlášeným uživatelům. Samozřejmě se dá napsat skript, který dostane název stahovaného souboru v parametru a pošle ho uživateli. V tom případě ale musíme zároveň zajistit, aby byl soubor fyzicky uložen v adresáři nepřístupném z webu, jinak hrozí, že uživatel stahovací skript obejde a stáhne si soubor bez přihlášení.
Pokud je seznam oprávněných uživatelů omezený, dá se autorizace vyřešit na straně Apache. Pokud jsou oprávnění uživatelé uložení v databázi, dá se použít buď neoficiální modul nebo můžeme adresu, na které jsou soubory skutečně uloženy, přepsat na autorizační skript pomocí mod_rewrite. To se také hodí v případě, kdy soubory byly v minulosti k dispozici bez ověření a toto ověření chceme dodatečně doplnit.
<?php
// RewriteRule ^data/ data.php [L,QSA]
$filename = substr($_SERVER["REDIRECT_URL"], 1);
if (!overit_pristup() || ($filename && (strpos($filename, "..") !== false || $filename[0] == "/" || !is_file($filename)))) {
header('WWW-Authenticate: Basic realm="data"');
header('HTTP/1.0 401 Unauthorized');
} elseif ($filename) {
header("Content-Type: application/octet-stream");
header("Content-Length: " . filesize($filename));
readfile($filename);
exit;
}
?>
Diskuse
Zdravím a posílám nástin alternativního řešení:
1) Ukládat informace o souborech při uploadu do databáze (název, přípona, typ a cokoliv dalšího)
2) Soubory fyzicky ukládat pod unikátním hashovaným názvem bez přípony, přičemž takový název je samozřejmě odvozen z databázového záznamu a je v zásadě neuhádnutelný.
3) Soubory stahovat prostřednictvím skriptu s parametrem jako např. soubor.php?cislo=[cislo_souboru] který nejdříve ověří práva uživatele a teprve poté mu soubor vydá:
<?php
// kontrola cache kvůli problémům s HTTPS v IE
session_cache_limiter('public');
header("Cache-Control: public");
header("Pragma: public");
// vrácení souboru
header("Content-Description: File Transfer");
header("Content-Length: ". $ulozena_velikost);
header("Content-Disposition: Attachment; filename=". $ulozeny_nazev .".". $ulozena_pripona);
@readfile($hashovany_nazev);
// znak @ je před funkcí readfile proto, aby nemohl být
// kompromitován název souboru ani při chybě skriptu
?>
Podotýkám, že při použití výše uvedeného postupu se ani přihlášený uživatel nemůže dostat ke skutečnému názvu souboru či souborů.
1) Ukládáním souborů s hashovanými názvy je zbytečné a o mnoho to nezvyšuje bezpečnost nad tím, když byste ukládal soubory s nějakými normálními (nejlépe původními) názvy - jen se Vám zvyšuje nepřehlednost. Sice si zase v PHP můžete vytvořit správce, který Vám tyto soubory ukazuje s původními názvy, ale tím příklad kouzlo jednoduchosti a geniality.
2) Pokud Vám příjde lepší volání stazeni_souboru.php?soubor=253256, tak potom jste asi nepochopil nejméně třetinu Jakubových článků.
Proč nevolat např.: /soubory/smlouva_o_pronajmu.doc a programově si ošetřit ověřování pěkně na pozadí??? Dobře zabezpečený server vaše soukromí ochrání.
Je to také možné řešení, ale není tak elegantní.
Navíc co mají všichni pořád s tím hašováním? Pokud chci nějaký náhodný identifikátor, tak přeci použiji náhodné číslo nebo náhodný řetězec, ne?
Díky za připomínky, byl to jen nástin alternativního řešení, nic víc nic míň :-) Robo
ady:
Predpokladam ze HASH pouzivaji v pripade kdy kuprikladu chteji, aby se stejny input vzdy prevedl na stejny hashovaci retezec, zatimco nahodne cislo vam pro vicekrat poslany totozny input vrati pokazde jiny result.
Takze pocitam, ze chteji udelat neco jako /download.php?jmeno_mojeho_souboru a ten aby sahal nekam do zvenku nepristupne slozky na soubor "muj_hash"
V tom případě může být soubor uložen rovnou pod původním názvem. Robo to zmiňoval jako možnost skrytí původního názvu, na což je to nevhodné.
qwe:
Taky mi připadá zbytečné hashovat nebo jinak názvy. Stačí soubory uložit do adresáře, kam je "Deny from all" z webu.
MichaL:
Delam to stejne, je to OK ze nemusim resit kolizi pri uploadu souboru stejneho nazvu.
URL souboru je pak treba domena.cz/soubor/{hash}/{puvodni-nazev}
Oldis:
kolizi nazvu resi uploadovany_soubor.jpg, id_z_db_ukladany_soubor_na_fs.jpg, hash mi prijde zbytecny
Petr Kozelek:
Prováděl bych ještě detekci na $_SERVER['HTTP_RANGE'] v případě, že by uživatel chtěl navázat na předchozí přerušené stahování...tak ať se mu posílá jen část souboru, kterou požaduje.
<?php
$size = filesize($filename);
if(isset($_SERVER['HTTP_RANGE']))
{
list($a, $range)=explode("=",$_SERVER['HTTP_RANGE']);
//if yes, download missing part
str_replace($range, "-", $range);
$size2=$size-1;
$new_length=$size2-$range;
header("HTTP/1.1 206 Partial Content");
header("Content-Length: $new_length");
header("Content-Range: bytes $range$size2/$size");
}
else
{
$size2=$size-1;
header("Content-Range: bytes 0-$size2/$size");
header("Content-Length: ".$size);
}
?>
Více na
http://www.phpbuilder.com/board/showthread.php?threadid=10318152.
kozotvor:
Zdravím,
pokud mám třeba gigovej soubor, který takto zpřístupňuji (přes readfile()), znamená to, že skript běží po celou dobu stahování? I po uběhnutí (defaultních) 30 vteřinách, po kterém proces "típne" samotné PHPko?
Jakub Vrána :
readfile() samo o sobě moc paměti nezabírá, protože data posílá rovnou na výstup. Problém by mohl spočívat v zapnutém output bufferingu.
Milan:
Dá se takto přesměrovaný soubor kontrolovat pomocí file_exists()?
Jakub Vrána :
Lokálně samozřejmě ano, vzdáleně ne – je to stejné jako s normálními soubory.
pojízdná kočka:
Zdravím.
Jak je v daném příkladu řešeno ošetření proti podvržení jména souboru pokusem o escapování pomocí pravidel pro tvorbu URL? a) dá se podvrhnout takové jméno souboru, které by dokázalo obejít kontrolu na ".." , "/" a b) co soubor, který obsahuje znaky, které byly url-encodovány?
Díky
Jakub Vrána :
Kontroluje se ta hodnota, která se následně používá pro získání názvu souboru. Takže pokud by útočník např. znak / zakódoval, tak se bude hledat soubor s názvem zakódovaného lomítka. Funkce readfile() už žádné dekódování nedělá.
Názvy souborů obsahujících speciální znaky se musí náležitě zakódovat. Např. k souboru a% se přistoupí přes URL a%25.
Michal:
Zdravím po delší době. Mám problém s hlavičkou:
header("Content-Disposition: Attachment; filename=xxx.xxx");
Když dám jako filename soubor s mezeru v názvu, tak se za posledním znakem před mezerou ořízne, např "Mateřská školka.txt" a bude tam jen "Mateřská". Diakritiku to bere v pořádku. Neví někdo proč ?
Emil:
Musíš to dát do uvozovek, např:
header("Content-Disposition: Attachment; filename=\"xxx.xxx\"");
zpětná lomítka escapují php rětězec
Vozka:
Ahoj, je možné nějak zjistit, zda byl soubor stažen komplet celý, že nebylo stahování z nějakého důvodu přerušeno?
U nějakých download managerů jsem to viděl, ale nikde se mi nedaří najít jak je to dělané. :-(
Předem díky
Jakub Vrána :
Pokud server pošle hlavičku Content-Length, tak stačí její obsah porovnat s velikostí staženého souboru. Pokud se používá Transfer-Encoding: chunked, tak se to dá poznat při stahování podle posledního chunku (měl by být nulový).
Diskuse je zrušena z důvodu spamu.