Using PHP to display images from an undisclosed folder

If you use PHP to upload images to your web server, it's normally necessary to set permissions on the upload folder to allow all users global access (read, write, and execute or 777 on *nix systems). This is the least secure setting, leaving it vulnerable to attack by malicious users. However, you can keep the location of the upload folder secret by using PHP to display the images in your web pages.

Nonsense, you might think. Just look at the source code of the web page, and it shows you the location of the image. Not if you use a proxy script, it doesn't. A proxy script has the added advantage that the upload folder doesn't even need to be inside your site root. As long as PHP has permission to read files, the folder can be just about anywhere.

This is how it works...

Instead of linking directly to the image in your web page, the <img> tag links to the proxy script, and passes the name of the image as a query string. So, let's say you want to display mypic.jpg, the <img> tag looks something like this:

<img src="proxy.php?img=mypic.jpg" alt="This is drawn from a hidden folder" ... />

Simple. So, how does the proxy script work? Let's build it stage by stage, and examine it as we go along.

  1. To be able to find the image, PHP needs to know the full path to the folder where images are being stored. The filename will be added to the end, so the path must end in a trailing slash. Use the physical path, not a URL. This is particularly important if the folder is outside the site root. Assign the path to a variable like this:

    $image_folder = '/home/mydomain/upload_folder/';

  2. Now, get the filename from the query string. Because it's passed to the proxy script through a URL, you can retrieve it from the $_GET array. However, it's important to test the value to make sure that an attacker isn't trying to probe your file system. You do this by passing the value to basename() and comparing it with itself. basename() extracts the filename from a path, so this prevents anyone from trying to pass a value such as ../../../etc/pwd. Add the following code:

    if (isset($_GET['img']) && basename($_GET['img']) == $_GET['img']) { $pic = $image_folder.$_GET['img'];

  3. The next test is to ensure that the file exists and is readable. PHP provides two intuitively named functions to do just that:

    if (file_exists($pic) && is_readable($pic)) {

  4. Assuming that the file is OK, you need to determine what type of image has been requested. This is necessary to send the correct HTTP headers to the browser. If you don't do this, visitors to your site will see an unsightly string of apparently random characters instead of a beautiful image. Use the PHP substr() function, and pass it -3 as the second argument to extract the last three characters of the filename. (A negative number as the second argument to substr() returns the same number of characters counting from the end of the string passed as the first argument.)

    $ext = substr($pic, -3);

  5. Pass the value of $ext to a switch() statement to set the correct MIME type. Since there's a possibility that an attacker might be trying to trigger an error message to probe your system, set any unknown filename extension to false.

    switch ($ext) { case 'jpg': $mime = 'image/jpeg'; break; case 'gif': $mime = 'image/gif'; break; case 'png': $mime = 'image/png'; break; default: $mime = false; }

  6. If one of the image filename extensions was found, $mime will have a value, which PHP equates to true. So, another condition test checks $mime before allowing the script to proceed any further. If it equates to true, it prepares the headers to send to the browser. One sets the Content-type header, the other tells the browser how big a file to expect, using the filesize() function.

    if ($mime) { header('Content-type: '.$mime); header('Content-length: '.filesize($pic));

  7. Finally, use fopen() to open the file in read binary mode, and stream it to the browser using fpassthru():

    $file = @ fopen($pic, 'rb'); if ($file) { fpassthru($file); exit; } } } }

The full code for proxy.php, complete with comments, looks like this:

<?php // define absolute path to image folder $image_folder = '/home/mydomain/upload_folder/'; // get the image name from the query string // and make sure it's not trying to probe your file system if (isset($_GET['pic']) && basename($_GET['pic']) == $_GET['pic']) { $pic = $image_folder.$_GET['pic']; if (file_exists($pic) && is_readable($pic)) { // get the filename extension $ext = substr($pic, -3); // set the MIME type switch ($ext) { case 'jpg': $mime = 'image/jpeg'; break; case 'gif': $mime = 'image/gif'; break; case 'png': $mime = 'image/png'; break; default: $mime = false; } // if a valid MIME type exists, display the image // by sending appropriate headers and streaming the file if ($mime) { header('Content-type: '.$mime); header('Content-length: '.filesize($pic)); $file = @ fopen($pic, 'rb'); if ($file) { fpassthru($file); exit; } } } } ?>

Other Tutorials

Programming tutorials

Articles on Adobe.com

Over the years, I have contributed a large number of articles to the Adobe Developer Connection and Community publishing. Most of the articles are now in the Adobe archive because they refer to old versions of Dreamweaver. But the following articles are not Dreamweaver-specific and are still relevant.

Books & Videos by David Powers

PHP SolutionsBeginning CSS3Dreamweaver CS6: Learn by VideoDreamweaver CS5.5: Learn by VideoDreamweaver CS5.5 for MobileDreamweaver CS5 with PHP