webForumDet fria alternativet

Kan man hindra access till filer och mappar med php sessions?

PHP

15 svar · 4 974 visningar · startad av bassebhu

Medlem sedan nov. 20016 480 inlägg
Frågan#1

Har en liten portal som ligger bakom en php session-inloggning. Är det möjligt att med samma session skydda åtkomst till bilder, mappar och andra filer när man inte är inloggad? Samt se till att inga filer sparas i webbläsarens cacheminne?

Medlem sedan aug. 20039 340 inlägg
#2

Du kan ju med hälp av en .htaccess rewrite se till att alla anrop till dessa filer går genom ett PHP-skript. Sedan kan du skicka filen genom att skicka lite headers och sedan anropa readfile, som skickar innehållet i en angiven fil. Dessa två filer ska alltså ligga i samma katalog där de uppladdade filerna ligger.

Lite sunkig exempelkod som jag har använt:

.htaccess:

RewriteEngine on

RewriteRule ^.*\.(jpg|jpe|jpeg|png|gif|mp3)$    getimg.php?path=$0 [NC]

getimg.php

<?PHP

function error($detail){
        header('content-type: text/html');
        die(htmlspecialchars($_GET['path']) . "<br>$detail");
}

function dumpfile($imgpath, $filename, $ext){
        $mime = "";

        switch(strtolower($ext)){
        case 'jpg':
        case 'jpeg':
        case 'jpe':
                $mime = 'image/jpeg'; break;
        case 'gif':
                $mime = 'image/gif'; break;
        case 'png':
                $mime = 'image/png'; break;
        case 'mp3':
                $mime = 'audio/mpeg'; break;
        default:
                //error("Invalid file type");
                error("404 File Not Found");
        }

        if (!file_exists($imgpath))
                error("404 File Not Found");

        header("Content-Type: ${mime}");
        header("Content-Disposition: inline; filename=${filename}.${ext}");
        // Add header lines to control the client caching here.
        readfile($imgpath);
}

$imgpath = $_GET['path'];

// Parse the enclosed filename
preg_match('/^'      . // Start of expr
'([^\\/].*\\/)?'      . // 1 Directory (Must be further validated for ..)
'([^\\/]+)\\.'       . // 2 Filename body
'([^\\/^.]{2,4})'    . // 3 File ext
'$/i'                  // End of expr
, $imgpath, $matches);

$dir = $matches[1];
$filename = $matches[2];
$ext = $matches[3];

if (false!==strpos($dir, '..')){
        error("Invalid path");
}

if ('getimg.php' == $imgpath){
        error("403 Forbidden");
}

$allowed = true;    // Figure out whether the user is allowed to see the file

if($allowed){
        dumpfile($imgpath, $filename, $ext);
}else{
        error("403 Forbidden");
}

(Koden ovan är något redigerad för att vara presentabel för allämnheten. Jag ber om ursäkt för eventuella syntaxfel pga redigering.)

Det finns mycket som kan gå fel, t ex:

Felmeddelanden från PHP kommer skickas innan filen och korrumpera innehållet. Lista ut något sätt att hantera detta, eller se till att sriptet aldrig gör några fel. ;)

Du vill inte att tecken ska skrivas ut efter att filen har skickats. Utelämna därför PHP-sluttaggen ?> sä slipper du att det skickas en extra radbrytning av misstag.

Säkerhet: Se till att en man inte kan komma åt andra filer genom att (t ex) anropa skriptet direkt med getimg.php?path=../databasepassword.php eller något. Jag tror att min validering ovan åstadkommer detta, men jag är öppen för förslag om motsatsen.

Säkerhet: Se till att man inte kan ange ett fint filnamn som skrivs ut i felmeddelandet om att filen inte hittas och tillåter skriptkörning, typ:
http://example.com/files/\<script%20src="http://ondsida.se/usch.js">.jpg
Jag tror att htmlspecialchars skyddar mot denna attack, men jag är öppen för förslag om motsatsen.

Säkerhet: Se till att skriptet för att ladda upp filer har skydd som gör att .htaccess och getimg.php inte kan skrivas över. (Dock bör ett sådant filter bara tillåta att ett litet urval av filtyper kan laddas upp, och .php bör ändå inte ingå i den gruppen, men ändå.)

Säkerhet: Notera att det reguljära uttrycket i .htaccess-filen bara matchar vissa filtyper, och t ex en PDF-fil skulle inte gå genom skriptet alls. Du kanske istället borde använda ett uttryck som matchar allt utom getfile.php (för att undvika att regeln skapar en oändlig loop.

Som sagt, om du ska använda en lösning i det här stuket så får du hålla tungan rätt i mun eftersom det är mycket som kan gå fel. :)

Ett bättre system kanske skulle hålla reda på alla filer i en databas och bara tillåta hämtning av filer som finns listade i databasen. Jag gissar dock att du sälv laddar upp filer via FTP, som inloggade användare kan komma åt.

Medlem sedan nov. 20016 480 inlägg
#3

Tack. hm, mycket att tänka på. Kan man spara ljud och videofiler till en mysql-databas också?

Det skulle ju kunna vara en lösning för bilder iaf. Alltid laddat upp bilder till servern via php, ej till databas. Tips på var jag kan läsa om det?

Medlem sedan nov. 20016 480 inlägg
#4

Coolt, det verkar funka! :D

Enda grejen nu är att när jag är utloggad så syns inte mina bilder på login-sidan, men det borde vara nån enkel workaround för det?

Medlem sedan aug. 20039 340 inlägg
#5

Nej, hela grejen med min lösning är att den går helt genom PHP. Du skulle kunna modda skriptet (eller be mig göra det) så att PHP skriver ut directory listingen. Du skulle också kunna låta listningen alltid vara på, oavsett om man är inloggad, så att bara nerladdning av filerna är skyddat.

Ett helt annat alternativ är att sätta en kaka som via .htaccess styr tillgång till både filerna och listningen, och alltså inte involvera PHP i nerladdningen av individuella filer. Nackdelen är då att någon som är inloggad skulle kunna dela med sig av kakan och då kan vemsomhelst som har tillgång till kakans hemlighet komma åt filerna.

Detta kan göras på olika mycket sofistikering. Antingen kollar du om en kaka har ett visst konstant värde och styr åtkomsten baserat på det. Eller så kan du skapa en hemlighet per användare och redigera .htaccess-filen från skriptet. Men det känns hackigt. Exempelvis kan det skapa problem om webbhotellet har en lagringslösning där det kan ta några sekunder från att du skriver till filen till att webbservern reagerar på innehållet.

Det finns säkert något "rätt" sätt att göra allt detta på, som jag inte känner till.

När jag pratade om att lagra något i en databas så menade jag bara att du skulle lagra ett index över filerna i databasen, inte själva filerna.

Medlem sedan aug. 20039 340 inlägg
#6

bassebhu skrev:

Enda grejen nu är att när jag är utloggad så syns inte mina bilder på login-sidan, men det borde vara nån enkel workaround för det?

*Host*
... och om jag faktiskt skulle ta och läsa vad det står i inlägget! Jag läste det där, slarvigt, som att du ville att filerna skulle synas om du gick till http://www.example.com/files/ eller liknande.

Att bilderna inte syns beror ju på att de faktiskt ligger i samma mapp som de andra filerna. Lösningen är att flytta bilderna till en annan mapp. Om de skyddade filerna + .htaccess och getimg.php ligger i /files/ lägg bilder och andra filer som alltid ska vara åtkomliga i /publicfiles/ (eller liknande)

Om bilderna ifråga verkligen måste ligga i samma mapp kan du istälelt lägga till en regel om att dessa filer ska undantas från regeln

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !header.jpg
RewriteCond %{REQUEST_FILENAME} !footer.jpg
RewriteRule ^.*\.(jpg|jpe|jpeg|png|gif|mp3)$    getimg.php?path=$0 [NC]
Medlem sedan nov. 20016 480 inlägg
#7

Ah, såklart. Flyttade .htaccess och min php-fil till mappen som jag vill skydda. Tack!

Ungefär hur säker är denna lösning? :)

Medlem sedan aug. 20039 340 inlägg
#8

bassebhu skrev:

Ungefär hur säker är denna lösning? :)

Ungefär lagom säker, om jag inte har gjort något allvarligt misstag. Säker nog att jag ska lita på den själv. Jag använder den personligen inte för att hårt begränsa åtkomsten till filer, utan för att dynamiskt motverka hotlinking. (Stänga av vissa sidor från att hetlänka, och gör andra mer avancerade saker.) Så mitt fokus är snarare på att inte öppna nya luckor med skriptet, och där tror jag att jag har lyckats bra.

Men för att besvara frågan, så nämnde jag ju att regexen bara matchar vissa filtyper. En PDF-fil skulle alltså inte ens behandlas av skriptet utan alltid vara tillgänglig. För att råda bot på detta, ändra .htaccess-filen till:

RewriteEngine on

RewriteCond %{REQUEST_FILENAME} !getimg.php
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.+$        getimg.php?path=$0 [NC]

Options -Indexes

Den sista raden stänger av fillistning. Jag tror iofs att Apache nuförtiden gör detta som standard, men det skadar inte att vara på den säkra sidan.

Medlem sedan nov. 20016 480 inlägg
#9

Ok, got it! Tack!

Medlem sedan nov. 20016 480 inlägg
#10

En sista sak. Med din senaste htaccess-kod skyddar jag alla filer oavsett filändelse, men bör jag ändra något i getimg.php också för att hitta rätt filtyp?

Jag vill kunna tillåta bl.a. .doc, .pdf, .wav, .aif, .mov, .m4a, mp4, m4v m.fl.
Lägger jag till alla dem i listan nedan eller kan den räkna ut det själv på nåt sätt?
Kanske finns någon site som listar alla filtyper med resp. mime som jag kan kolla upp.

switch(strtolower($ext)){
        case 'jpg':
        case 'jpeg':
        case 'jpe':
                $mime = 'image/jpeg'; break;
        case 'gif':
                $mime = 'image/gif'; break;
        case 'png':
                $mime = 'image/png'; break;
        case 'mp3':
                $mime = 'audio/mpeg'; break;
        default:
                //error("Invalid file type");
                error("404 File Not Found");
        }
Medlem sedan aug. 20039 340 inlägg
#11

Just ja, det hade jag glömt. Om filen som efterfrågas har en okänd filändelse visas den bara som 404 not found. Idén var att gör en exercis i säker kodning, dvs att om en del fallerar ska en del stå emot. T ex, om någon lyckas få igenom ett filnamn utanför katalogen ifråga på något sätt så kommer man ändå nekas åtkomst eftersom de flesta intressanta filer för hackare inte är bilder och mp3-filer, utan php-filer eller systemfiler.

Listor över mime-typer finns det gott om. Googla mime list och välj och vraka. Notera för övrigt att du kan gruppera flera case-satser som i exemplet med jpg. Detta eftersom oden "faller igenom" (fall-through). Det är också därför du behöver en break-sats i slutet av ett inlägg. (Du, dvs basse, vet säkert detta, men andra läser säkert också denna tråd. :) )

Du kan för det mesta sätta mime-typen till application/octet-stream om standardvärde så får webbläsaren lista ut vad den ska göra med filen. Sen beror det på vad du vill ska hända med filen.
Om du vill uppmuntra webbläsaren att ladda ner filen, ätt $download till true i de case där detta är vad du vill, som i mp3 i exemplet nedan.
Om du vill uppmuntra webbläsaren att visa filen i webbläsarfönstret, så som du kan göra med t ex ljud, video och PDF-filer så bör du sätta mime-typen rätt, och $download till false.

function dumpfile($imgpath, $filename, $ext){
        // Default values
        $mime = "application/octet-stream";
        $download = false;

        switch(strtolower($ext)){
        case 'jpg':
        case 'jpeg':
        case 'jpe':
                $mime = 'image/jpeg';
                break;
        case 'gif':
                $mime = 'image/gif';
                break;
        case 'png':
                $mime = 'image/png';
                break;
        case 'mp3':
                $download = true;
                $mime = 'audio/mpeg';
                break;

        default:
                $download = true;
        }

        if (!file_exists($imgpath))
                error("404 File Not Found");

        header("Content-Type: ${mime}");

        if ($download){
                header("Content-Disposition: attachment; filename=${filename}.${ext}");
                header('Content-Transfer-Encoding: binary');
        }else{
                header("Content-Disposition: inline; filename=${filename}.${ext}");
        }

        // Disable caching
        header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1.
        header('Pragma: no-cache'); // HTTP 1.0.
        header('Expires: 0'); // Proxies.

        readfile($imgpath);
}

För övrigt, jag rekommenderar även att lägga till raderna som stänger av cachening till error-funktionen. Om felsidan av någon anledning cacheas så kan det vara irriterande för användaren som t ex måste aldda om sidan.

function error($detail){
        header('content-type: text/html');
        // Disable caching
        header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1.
        header('Pragma: no-cache'); // HTTP 1.0.
        header('Expires: 0'); // Proxies.

        die(htmlspecialchars($_GET['path']) . "<br>$detail");
}

Pust! Det var nog allt!

Medlem sedan nov. 20016 480 inlägg
#12

ok, cool. funkar. tack! :D

Medlem sedan nov. 20016 480 inlägg
#13

Hej! Jag igen :)

Har fulat ner din kod lite! Det funkar hyfsat bra som jag vill, men det som jag stöter på problem med är ljud och videofiler. Speciellt när jag kör dem genom en html5- eller flash-spelare. Utan scriptet påslaget rullar det bra, men med det påslaget så hänger sig webbläsaren, den verkar inte få något grepp om hur lång ljudfilen är m.m. Utan scriptet visar min spelare in och uttid, med det påslaget så står det bara N/A för både in och ut.

När jag testar att ladda en mp3 direkt i Safari så spelas den med timeline och allt med scriptet av, men med det på syns ingen timeline och det står bara "Direktsändning" där i stället.

Går det att få mediafilerna att agera precis likadant med scriptet på och av? Antar att allt header-type-mime-grejs rör till det för webbläsaren.

Tack för tips!

Så här ser min kod ut just nu:

RewriteEngine on

RewriteCond %{REQUEST_FILENAME} !getmedia.php
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.+$        getmedia.php?path=$0 [NC]

Options -Indexes
<?php

session_start();
header("Cache-control: private");

function error($detail) {
        header('content-type: text/html');
        // Disable caching
        header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1.
        header('Pragma: no-cache'); // HTTP 1.0.
        header('Expires: 0'); // Proxies.

        die(htmlspecialchars($_GET['path']) . "<br>$detail");
} 

function dumpfile($filepath, $filename, $ext) {
        // Default values
        $mime = "application/octet-stream";
        $download = false;

        switch(strtolower($ext)) {
        case 'jpg':
        case 'jpeg':
        case 'jpe':
                $mime = 'image/jpeg';
                break;
        case 'gif':
                $mime = 'image/gif';
                break;
        case 'png':
                $mime = 'image/png';
                break;
        case 'tiff':
        case 'tif':
                $download = true;
                $mime = 'image/tiff';
                break;
        case 'pdf':
                $mime = 'application/pdf';
                break;
        case 'zip':
                $download = true;
                $mime = 'application/zip, application/octet-stream';
                break;
        case 'rar':
                $download = true;
                $mime = 'application/x-rar-compressed, application/octet-stream';
                break;
        case 'doc':
                $download = true;
                $mime = 'application/msword';
                break;
        case 'docx':
                $download = true;
                $mime = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
                break;
        case 'xls':
                $download = true;
                $mime = 'application/vnd.ms-excel';
                break;
        case 'ppt':
                $download = true;
                $mime = 'application/vnd.ms-powerpoint';
                break;
        case 'eps':
                $download = true;
                $mime = 'application/postscript';
                break;
        case 'mp3':
                $mime = 'audio/mpeg';
                break;
        case 'wav':
                $mime = 'audio/wav';
                break;
        case 'm4a':
                $mime = 'audio/mp4';
                break;
        case 'ogg':
                $mime = 'audio/ogg';
                break;
        case 'oga':
                $mime = 'audio/ogg';
                break;
        case 'webma':
                $mime = 'audio/webm';
                break;
        case 'wma':
                $mime = 'audio/x-ms-wma';
                break;
        case 'wmv':
                $mime = 'video/x-ms-wmv';
                break;
        case 'webmv':
                $mime = 'video/webm';
                break;
        case 'ogv':
                $mime = 'video/ogg';
                break;
        case 'avi':
                $mime = 'video/avi';
                break;

        default:
                $download = false;
        }

        if (!file_exists($filepath))
                error("404 File Not Found");

        header("Content-Type: ${mime}");

        if ($download){
                header("Content-Disposition: attachment; filename=${filename}.${ext}");
                header('Content-Transfer-Encoding: binary');
        }else{
                header("Content-Disposition: inline; filename=${filename}.${ext}");
        }

        // Disable caching
        header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1.
        header('Pragma: no-cache'); // HTTP 1.0.
        header('Expires: 0'); // Proxies.

        readfile($filepath);
}

$filepath = $_GET['path'];

// Parse the enclosed filename
preg_match('/^'      . // Start of expr
'([^\\/].*\\/)?'      . // 1 Directory (Must be further validated for ..)
'([^\\/]+)\\.'       . // 2 Filename body
'([^\\/^.]{2,4})'    . // 3 File ext
'$/i'                  // End of expr
, $filepath, $matches);

$dir = $matches[1];
$filename = $matches[2];
$ext = $matches[3];

if(false!==strpos($dir, '..')) {
        error("Invalid path");
}

if('getmedia.php' == $filepath) {
        error("403 Forbidden");
}

if(isset($_SESSION['login_status']))
{
	if ($_SESSION['login_status'] == true)
	{
		// User is logged in!
		dumpfile($filepath, $filename, $ext);
	}
	else
	{
		// User isn't logged in
		error("403 Forbidden");
	}
}
Medlem sedan nov. 20016 480 inlägg
#14

Någon som har en idé om vad det kan vara?

Jag testade även att använda funktionen för att ladda ner filer ($download = true) och på de flesta får jag nedladdade filer utan filändelser och med ett pappersark som ikon. T.o.m. på jpg och såna vanliga format. Filerna laddas ner och de har rätt storlek, men det verkar som att den inte känner av vilket format det är. Kan det vara för att jag har mellanslag i filnamnen så att man måste köra nån urlencode eller nåt sånt?

Jag kör lokalt på min mac.

Tack för hjälp :birp

Medlem sedan nov. 20016 480 inlägg
#15

Update: För min andra fundering så verkar det bara vara Firefox som laddar ner filerna utan filtyp (v.25.0.1). Safari och Chrome laddar ner korrekt.

Någon som vet vad Firefox har för sig och om man kan komma runt det?

Mitt problem med att ljud och videofiler tolkas olika beroende på om scriptet är på eller av kvarstår dock. Min HTML5-mediaspelare kan inte hantera filer hämtade genom scriptet av nån anledning.

Medlem sedan nov. 20016 480 inlägg
#16

Hej igen :P

Kan man på nåt sätt tala om att om det finns en php-session så behöver inte scriptet köras? Det funkar som sagt hyfsat förutom det med att mediafiler inte kan spelas genom html5-spelare, men det kräver ju sin lilla tid att hämta filerna genom php.

Kan man inte på nåt enkelt sätt säga att om en php-session inte är satt så kommer man inte åt något i en bestämd mapp? Integrerat med htaccess eller inte.

Tack för förslag!

261 ms totalt · 4 externa anrop · v20260731065814-full.e96017d9
123 ms — deklarationer (db)
0 ms — hämta statistik (cache)
136 ms — hämta tråd, inlägg och bilagor (db)
120 ms — ändringar (db)