Document security with sessions

PHP programming forum. Ask questions or help people concerning PHP code. Don't understand a function? Need help implementing a class? Don't understand a class? Here is where to ask. Remember to do your homework!

Moderator: General Moderators

Post Reply
timj
Forum Newbie
Posts: 14
Joined: Tue Aug 26, 2003 8:00 am
Location: Belgium

Document security with sessions

Post by timj »

Can anyone tell me how to make documents (pdf, etc) protected by a website login?
Suppose that I'm using a session (like in the very useful information already posted as "sticky").
Well, the session verification is nice for websites (.php pages), but it's impossible for other documents.
Instead of using links to the documents, I suppose I need a link to some script that presents a certain document (name is passed as a get or post variable) to the user if his session is OK.
But how can I make a PhP script present any document? Are there any limitations on the file formats that can be used?

And, regarding file security, what prevents any web user from opening the document by "accidentally" typing in the correct URL? (Of course, most site users won't know the filename/directory structure, but it seems unsafe to me anyway). Do I need to store them in a special -protected- directory, eg forbidden to the normal web user that Apache uses? How can I make PhP have access to a directory that is forbidden to the Apache user?

I'm using PhP 4.2.x and Apache 2.x, so sessions are supported.
e4c5
Forum Newbie
Posts: 3
Joined: Tue Sep 09, 2003 10:37 am

Post by e4c5 »

You can do this in two ways. One is to use .htaccess and the other involves placing your files outside the webspace. That way they cannot be downloaded directly. Then you create a php script that will fpassthru() the files if the session is valid.


You will need to throw out the correct header information of course.
timj
Forum Newbie
Posts: 14
Joined: Tue Aug 26, 2003 8:00 am
Location: Belgium

Post by timj »

Can you give me a little more information? I'll look at the fpassthru function that you mentioned, but I haven't used it before. And what about the headers? I've already seen plenty of people on the forum that are having trouble with the header (eg "header already sent"-errors). Is this what I should be aware of?
User avatar
JAM
DevNet Resident
Posts: 2101
Joined: Fri Aug 08, 2003 6:53 pm
Location: Sweden
Contact:

Post by JAM »

If you look at the manuals example at http://se.php.net/manual/en/function.fpassthru.php you'll see how you cuold use the function, sending files directly to the browser from a path outside the users access.

There are some header()'s there that might guide you in your quest.

About the header() problems, you should look at this viewtopic.php?t=1157 url. Sticky in the forum to describe it.
timj
Forum Newbie
Posts: 14
Joined: Tue Aug 26, 2003 8:00 am
Location: Belgium

Post by timj »

If I understood it correctly, the way I'm supposed to do it is:
-start session and check it
-set the headers (as in the snippets at the bottom of the manual page of fpassthru that you gave me)
-fopen my file in a binary read mode,
-fpassthru the file to read
And this will give me the chance to pass through the file to the client's browser, either "inline" or as attachment?
I need to store the files outside of the document root of the site, but accessible for reads to the user that Apache and PhP run as.

Is this correct?

In any case, thanks a lot for your help!
User avatar
JAM
DevNet Resident
Posts: 2101
Joined: Fri Aug 08, 2003 6:53 pm
Location: Sweden
Contact:

Post by JAM »

Sounds allright...
Give it a shot and let us know if you experience problems.

Good Luck.
timj
Forum Newbie
Posts: 14
Joined: Tue Aug 26, 2003 8:00 am
Location: Belgium

Post by timj »

Well, JAM and e4c5, it worked well.
I've put the documents in a specially protected directory outside of the document root and I've managed to open them with the PhP script.

For security, I'm only making it possible to read files in 1 directory (by prepending that path and checking if the file exists) and only allowing the files from a given list (so no filename as GET or POST parameter, otherwise someone may try to call up any file outside of the doc root).

There is only 1 problem. Well, since I'm using documents of many formats, I've written a little code to determine the mime type (for the Content-type header) based on the (last) file extension, eg a .pdf at the end will have the mime type application/pdf, etc.

I've tested this with a plain text file and it works fine, but when I try to open a pdf file the browser opens Acrobat 5, but then Acrobat reports that the file is "damaged" and cannot be opened. However, I can open the file with the acroread command from the command line (note: acroread starts Acrobat 4, not Acrobat 5). Can you explain this? Do I need to use the pdf_* functions for PDF files?

Thanks anyway for the big help! I couldn't have done this without you!
If this last issue can be resolved, I'll post the code and call this thread resolved.
User avatar
JAM
DevNet Resident
Posts: 2101
Joined: Fri Aug 08, 2003 6:53 pm
Location: Sweden
Contact:

Post by JAM »

I borrowed this code from another site once, and it worked for me. Rewrite to fit your needs (pay attention to the headers).
for the "forced download", I suggest using this :

Code: Select all

header("Content-disposition: attachment;;filename=$filename.txt");
header("Content-type: application/octetstream");
header("Pragma: no-cache");
header("Expires: 0");

$client=getenv("HTTP_USER_AGENT");
$fp=fopen("$filename","r");
$str=fread($fp,filesize($filename));
echo $str;
fclose($fp);
also possible : application/save
timj
Forum Newbie
Posts: 14
Joined: Tue Aug 26, 2003 8:00 am
Location: Belgium

Post by timj »

Well, it seems to work, but now I'm having trouble with plain Gif and jpeg images. That can't be caused by the browser plugin (as for pdf). When I click on the link that activates the getfile.php script (below), it gets the right file, but the image is only showed for a part. Is there some kind of limit on the file size or download time? This may be causing the problems with the PDF files as well (if the file isn't complete, Acrobat could report this as "being damaged").

Code: Select all

<?
  session_start();
  
  /*check session login, if user isn't logged in => send him to loginpage*/
  if(!isset($_SESSION['auth']))
  {  	
	header("Cache-control: private"); /*IE 6 Fix*/
  	header("Location: loginpage.php");
	die();
  }
  if(!$_SESSION['auth'])
  {
  	header("Cache-control: private"); /*IE 6 Fix*/
  	header("Location: loginpage.php");
	die();
  }

  if(!isset($_GET['filenr']))
  {
	header("Location: invalidfilenr.php");
	die(); 
  }
  else
  {
  //intval() is not necessary?
  	$fnr=intval($_GET['filenr']);
  }

  $disposition = "inline"; // "inline" to view file in browser or "attachment" to download to hard disk

  $protectedpath="/home/timj/mailfiles/php_ESACII/";
  $filelist=$protectedpath."document_list";

/*read all filenames in list*/
  $files=file($filelist);

  $nrFiles=count($files);

  if($fnr>$nrFiles-1 || $fnr<0)
  {	//fnr is too high or too low
	header("Location: invalidfilenr.php");
	die(); 
  }


//READ FROM LIST OF FILES INSTEAD
// $name = "foo.jpg";
// $path = "/path/to/foo.jpg";

  //file's name - use trim to strip off the newline at the end
  $name=trim($files[$fnr]);
  //file's name with full path
  $path=$protectedpath.$name;

  //get file's (last) extension - make sure it's in lowercase (to avoid case mistakes)
  $arr=explode('.',$name);
  $ext=strtolower(array_pop($arr));
  
  //SHOULD BE DECIDED BASED ON FILE THAT IS DEMANDED - use a switch() - default=octec_stream
  //$mime = "image/jpeg"; // or whatever the mime type is
  //mime type
  $mime='';
  
  switch($ext)
  {
      case 'jpg':
  	    //fall through => jpg is treated the same as jpeg because no "break"
      case 'jpeg':
   	    $mime='image/jpeg';
  	    break;
      case 'gif':
   	    $mime='image/gif';
    	    break;
      case 'tif':
  	    //fall through => tif is treated the same as tiff because no "break"
      case 'tiff':
     	    $mime='image/tiff';
  	    break;
      case 'png':
     	    $mime='image/png';
  	    break;
      case 'doc':
     	    $mime='application/msword';
  	    break;
      case 'ppt':
     	    $mime='application/vnd.ms-powerpoint';
  	    break;
      case 'pdf':
   	    $disposition="attachment"; //PDF are always saved to disk, not opened in browser
	    $mime='application/pdf';
  	    break;
      case 'txt':
     	    $mime='text/plain';
	    break;
      case 'xls':
     		    //fall through: xls is treated the same as xla (Excel)
      case 'xla':
	    $mime='application/vnd.ms-excel';
  	    break;
      case 'ai':
    		    //fall through: ai and eps are treated the same as ps (postscript)
      case 'eps':
    		    //fall through: ai and eps are treated the same as ps (postscript)
      case 'ps':
	    $mime='application/postscript';
  	    break;
      case 'mpg':
    		    //fall through: mpg and mpe are treated the same as mpeg (MPEG video)
      case 'mpe':
    		    //fall through: mpg and mpe are treated the same as mpeg (MPEG video)    
      case 'mpeg':
	    $mime='video/mpeg';
  	    break;
      case 'avi':
	    $mime='video/x-msvideo';
  	    break;
      default:
	    $mime='text/html';
	    //$mime='';
	    break; // is not necessary since "default" is the last case
  }
 
 //send headers
 
  if (isset($_SERVER["HTTPS"])) {
      /**
       * We need to set the following headers to make downloads work using IE in HTTPS mode.
       */
      header("Pragma: ");
      header("Cache-Control: ");
      header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
      header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
      header("Cache-Control: no-store, no-cache, must-revalidate"); // HTTP/1.1
      header("Cache-Control: post-check=0, pre-check=0", false);
  }
      else if ($disposition == "attachment") {
          header("Cache-control: private");
  }
  else {
      header("Cache-Control: no-cache, must-revalidate");
      header("Pragma: no-cache");
  }
  header("Content-Type: $mime");
  //trim eliminates spaces/tabs at beginning and end
  //htmlentities translates all special chars to html equivalents
  header("Content-Disposition:$disposition; filename="".trim(htmlentities($name)).""");
  header("Content-Description: ".trim(htmlentities($name)));
//  header("Content-Length: ".(string)(filesize($path)));
  header("Content-Length: filesize($path)");

/*should wait until file is passed through? - NO*/
  header("Connection: close");

if(file_exists($path))
{
	/*open file*/
	$fp=fopen($path,'rb');

	//check if fopen works? No: you can't give any output to browser because
	//mime-type is not text/html
	//if(!$fp)
	//AANP: headers pas NA fopen?

	/*pass through to browser*/
	fpassthru($fp);

	/*close file*/
	fclose($fp);
}
?>
timj
Forum Newbie
Posts: 14
Joined: Tue Aug 26, 2003 8:00 am
Location: Belgium

Post by timj »

Altough the technique seems fine, I'm having trouble showing the files inline in the browser.
A small plain text file (about 20 bytes) shows up fine, a 3k jpeg image is only partially shown,
a 30k gif image isn't shown at all and the large pdf is also not shown. However, saving the pdf document to disk does work (as I mentionned in my earlier post).

In my opinion, this could be caused by some time out limit or data size limit on the file transfers.
This could cause large files not to be shown, medium files (like the jpeg) to be partially shown and small files to be shown completely (because they are transferred in time or are small enough).

Do you know any parameters that I need to verify or change in php.ini or httpd.conf for this?

Thanks in advance!
User avatar
JAM
DevNet Resident
Posts: 2101
Joined: Fri Aug 08, 2003 6:53 pm
Location: Sweden
Contact:

Post by JAM »

What happens if you use the http://se.php.net/manual/en/function.ob-start.php functions combined with your script?
timj
Forum Newbie
Posts: 14
Joined: Tue Aug 26, 2003 8:00 am
Location: Belgium

Post by timj »

Sorry, no luck.

I've tried adding ob_start(); in the beginning (even before session_start()) and I've tested it with and without ob_end_flush() at the end.

The result is still the same.

I will now look in the apache and php errorlogs to see if I can find anything, but I doubt it.
timj
Forum Newbie
Posts: 14
Joined: Tue Aug 26, 2003 8:00 am
Location: Belgium

Post by timj »

I've tried a normal flush() at the end instead of ob_end_flush();
And this gives me an error message when I open the .gif and .jpeg (as if the documents are broken).
Post Reply