Skip to content.

Scott Arciszewski

Software, Privacy, Security, Innovation

Building a PHP Image Proxy Script

August 1, 2014 11:42 PM • Information Securty, Open Source, PHP, Security, Migrated, Tutorial

This was originally posted on a website I was developing over a year ago called Keenotes.


There is a fine line between useful and invasive. Today, I'm going to demonstrate something that can be used for either purpose: A PHP script that proxies images from a folder outside of your webroot folder.

But before we get to that, let's examine why we might want a PHP proxy in the first place:

  • When a user uploads a file, we way mish to give it a different file name, internally, than the one users see when they access it.
  • When a user uploads a file, we may wish to encrypt or encode it on the filesystem to ensure it never gets executed.
  • We may wish to collect usage statistics on where the file is linked from, to determine hotlinking and whatnot.
    • And also serve an alternative image for known offending websites
  • We may wish to serve a random image from a collection (special case, not part of this example).
  • We may wish to use unique image URLs to determine someone's IP address to track them down (for example, to track down the identity of a cyber-bully using a bit of social engineering).

In my examples, I will use a setup that looks something like this on an nginx webserver with php5-fpm and PDO-sqlite enabled:

[email protected]:/var/www# ls
  global_icons
  site.com
  site2.com
  site3.com
[email protected]:/var/www# ls site.com
  cli_scripts
  files.db
  includes
  public_html
  uploaded

For starters, we are going to create an nginx rewrite rule for the site1.com configuration (you can do this with Apache too) that looks like this:

rewrite ^/uploads/([^/]+)$ /myUploadProxy.php?name=$1;

That is to say, when someone accesses http://site.com/uploads/file.gif they are really accessing /var/www/site.com/public_html/myUploadProxy.php with the name=file.gif parameter passed to the script. Do you follow me so far?

Now, at this point, we want to serve images from /var/www/site.com/uploaded. All requests to the uploads folder are being redirected toward /var/www/site.com/public_html/myUploadProxy.php so all we have to do is read data from the correct folder and return it to the user, and we will have a simple image proxy script.

<?php
define('BASEDIR', '/var/www/site.com/uploaded');
$name = str_replace('/', '', $_GET['name']); // Filter out LFI
if(preg_match('/(http|https|ftp|irc):/', $name)) {
  header("Location: /404.html"); exit;
  // 404 Not Found is the best way to handle this
}
if(!file_exists(BASEDIR."/{$name}")) {
  header("Location: /404.html"); exit;
  // File doesn't exist; you can do more flexible stuff here, like having a 404 image fallback
}
// Image found at this point. Let's continue by creating a finfo resource to determine mime types
$f = finfo_open(FILEINFO_MIME, "/usr/share/misc/magic");
$type = finfo_file(BASEDIR."/{$name}");
// Now let's pass the header so the browser knows what we're getting back
header("Content-Type: {$type}; filename=\"{$name}\"");
// Then let's output the data stored in the file to the user and close up shop for this request :)
echo file_get_contents(BASEDIR."/{$name}");
exit;
?>

This script first determines that the file exists, determines its MIME type, and then outputs the file header and body to the user. Pretty simple, but our webserver could do that without having to slow things down by passing requests and image data through PHP. Why add any overhead if we're not going to benefit from it?

Here's a beefier, more secure image proxy. The only feature it's lacking is usage statistics and user metadata harvesting, because I feel if you're going to be a data leech and help spy on innocent end users, you can learn to do it without my help.

Changes made (and assumptions in the design):

  • The filesystem does not know the user-supplied filename; instead, random hashes were used for actual storage and an sqlite database holds all of the metadata
  • The files were gzip compressed to save space
  • File type is restricted by a whitelist; anything that fails to match the allowed types are returned as text/plain
  • Files are round-robined through 10 subdirectories in the uploaded files directory to increase filesystem performance (which helps to reduce DoS attack vectors)
<?php
define('BASEDIR', '/var/www/site.com/uploaded');
$name = str_replace('/', '', $_GET['name']); // Filter out LFI
if(preg_match('/(http|https|ftp|irc):/', $name)) {
  header("Location: /404.html"); exit;
  // 404 Not Found is the best way to handle this
}
if(!file_exists(BASEDIR."/{$name}")) {
  header("Location: /404.html"); exit;
  // File doesn't exist; you can do more flexible stuff here, like having a 404 image fallback
}
// Image found at this point. Let's continue by creating a finfo resource to determine mime types
$f = finfo_open(FILEINFO_MIME, "/usr/share/misc/magic");
$type = finfo_file(BASEDIR."/{$name}");
// Now let's pass the header so the browser knows what we're getting back
header("Content-Type: {$type}; filename=\"{$name}\"");
// Then let's output the data stored in the file to the user and close up shop for this request :)
echo file_get_contents(BASEDIR."/{$name}");
exit;
?>

Further Considerations / Ways to Improve this Design

  • Encrypt the filedata with AES-256-CTR, using a random key and IV (stored in the database), signed with HMAC-SHA-256 by a PHP constant to detect errors before serving to the end user
  • Proxy requests through CURL to another device on the local network that store all of the files
  • Track file usage (access frequency over time) with the database
  • Cache the most popular files with APC
  • Rate-limit large files that aren't cached to prevent DoS attacks
  • Incorporate user-level access controls for certain files so only authenticated users who uploaded the file or had it shared with them through application logic can download the file later

Downloads

If anyone has any other suggestions, feel free to comment below.

Blog Archives Categories Latest Comments

Want to hire Scott Arciszewski as a technology consultant? Need help securing your applications? Need help with secure data encryption in PHP?

Contact Paragon Initiative Enterprises and request Scott be assigned to your project.