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;
}
?>
Jakub Vrána, Řešení problému, 4.7.2008, diskuse: 22 (nové: 0)

Diskuse

Robert Vlach:

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ů.

ikona bukaJ:

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í.

ikona Jakub Vrána OpenID:

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?

Robert Vlach:

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"

ikona Jakub Vrána OpenID:

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}

ikona Jakub Vrána OpenID:

Tady se ale jedná o hash obsahu, nikoliv názvu.

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?

Tomáš Fejfar:

Přesně tohle jsem řešil. Jen v tomto případě byl problém s memory_limit :)

Nakonec sem to vyřešil a blognul o tom :) Neřikám, že tam ještě není někde nějaká chyba nebo exploit, ale byl oto to nejlepší, co sem z toho dostal ;)

https://blog.tomasfejfar.cz/zabezpecene-stahovani-souboru/

ikona Jakub Vrána OpenID:

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()?

ikona Jakub Vrána OpenID:

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

ikona Jakub Vrána OpenID:

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

ikona Jakub Vrána OpenID:

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.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.