4

I have a php file called gallery.php which is the page for a user specific gallery of images. For security, the user images are stored outside of web root.

To retrieve the appropriate files for each user, I use a getimage.php file which serves the images from their location. All in all, the directory structure looks like this:

  • UserImages
    • User1
      • List of user1's images
    • User 2
      • List of user2's images
  • public_html
    • gallery.php
    • getimage.php

getimage.php is written as follows:

$imgString = realpath('/UserImages/' . $_SESSION['username'] . '/' . $_GET['img']);
if (!startsWith($imgString, '/UserImages/' . $_SESSION['username'] . '/')
    || !(endsWith(strtolower($imgString), '.jpg') || endsWith(strtolower($imgString), '.jpeg')))
{
    header('HTTP/1.0 403 Forbidden');
    die();
}

$img = file_get_contents($imgString);
header("Content-type: image/jpeg");
echo($img);
exit();

I rely on $_GET['img'] to determine which image to retrieve, which is a possible security hole (and a major one at that). I could forsee a directory traversal attack, hence the use of realpath, though I'm sure there can be other avenues of attack I do not contain with this script.

For that reason, I'd prefer if I could move getimage.php outside of webroot, or at least prevent it from being accessible directly (and only through gallery.php, where the sent img parameter is strictly under my control).

Any time I try to move getimage.php oustide of public_html however, I can't seem to call it anymore even if I do a require or include in gallery.php. The way I access getimage.php is doing this:

<img src=getimage.php?img=IMG_FILENAME.jpg />

But getimage.php will fail if I ever move it out of the public_html directory.

So, long story short: what do I need to do to prevent getimage.php from being abused?

BurnerBoy
  • 93
  • 2
  • 9
  • since the images are to be served on the web, I don't think storing them outside of web root is a good idea, since in order to serve them you need to create a security hole anyway. Instead I would validate that the files uploaded are really images and then make them accessible to the web. http://stackoverflow.com/questions/6755192/uploaded-file-type-check-by-php – Joe T Dec 23 '14 at 04:40
  • I don't have any need for an upload check, since the files being served are all of my own making. The user simply needs to be able to see them. If I were to place the user images inside of web root, how do I prevent any one other than User1 accessing User1's files? – BurnerBoy Dec 23 '14 at 17:09
  • I'm confused on why you would need to secure some images. To me your script seems to be the problem here. Why not move your images to a subdomain (completely separate from your application) and serve them from there ? – piry Dec 31 '14 at 12:15
  • How would I prevent unauthorized users from accessing images they aren't supposed to by knowing what the URL is to another person's image file? – BurnerBoy Dec 31 '14 at 18:09

3 Answers3

3

The best secure way is to not pass image url using $_GET['img'], but send reference to the image instead.

The Problem

Firstly, $_GET['img'] can be any external url to a harmful code, and with file_get_contents you are downloading it directly.

Secondly, anyone can use your server as free image proxy by simply using the following url in his own website:

<img src="http://www.yourwebsite.com/getimage.php?img=[external_image_url]">

The Correct Way

1) In your database create images table containing paths to user images. When you want to serve an image pass the image ID instead of the url:

$imageID = $_GET['image'];
.... your sql query here to retrieve the image....
$img = file_get_contents($imgString);

2) Because you know that $_GET['image'] is an ID you can sanitize it easily:

if( !preg_match("/^[0-9]+$/", $_GET['image']) )
{
    header('HTTP/1.1 404 Not Found');
    exit();    
}

3) In your SQL dont forget to use prepared statments:

$sql = "SELECT image_path from images WHERE id = ?";

4) Only images should be moved outside document root folder, if you want to move getimage.php outside root as well you can use Apache Alias to serve it. In Apache Virtualhost config you can use:

Alias /getimage.php /path/to/getimage.php

5) Use 404 not found instead of 403 forbidden, don't give the attacker a hint that you caught his action.

DeepBlue
  • 684
  • 7
  • 23
1

You need to allow Apache (assumed) to access that folder with images using Directory directive (http://httpd.apache.org/docs/2.2/mod/core.html#directory).

Or you can move images under root directory and restrict direct access to them using .htaccess if you want to keep them protected.

Dukecz
  • 342
  • 2
  • 14
  • I wanted to use a solution like this, combined with a login page, but I didn't know how. Is there a way to use .htaccess combined with a login page and not that browser based pop-up box? – BurnerBoy Jan 07 '15 at 05:05
  • Sure you can. You can use rewrite (http://httpd.apache.org/docs/2.4/rewrite/flags.html) to forward all requests (except css, ico and js) to your page where you can have login and script to provide images with something like: "RewriteRule !\.(js|ico|css)$ index.php" in your .htaccess – Dukecz Jan 07 '15 at 22:44
1

The only way to do this using your directory structure is to clean the $_GET['img'] variable and ensure it doesn't have any unwanted characters.

You could do this with a regular expression which filters everything except Letters, number, underscores ,hyphens and dots.

Try replacing the first line of your code with this...

$imgString = realpath('/UserImages/' . $_SESSION['username'] . '/' . preg_replace("/[^a-zA-Z0-9._-]/", "", $_GET['img']));

Alternatively you could check to see if the $_GET['img'] variable contains bad characters with this simple If statement before your code.

if(preg_replace("/[^a-zA-Z0-9._-]/", "", $_GET['img']) != $_GET['img']){
    // Throw an error
    exit();
}

PS: don't forget to check your file exists before reading it or you will get errors if it doesn't. You could use the files existence as an additional security check.

RCrowt
  • 921
  • 10
  • 19