"Innovation comes only from readily and seamlessly sharing information rather than hoarding it."- Tom Peters
Development Blog

Protecting Images using PHP and htaccess

Image protection on the internet can be quite tough. With view source, and tools like Firebug, it takes a combination of tactics. In this article, we'll go over a few techniques that can be used.

Sources to check

In this article, we're going to tackle a few different tactics to safeguard images. But, let's first think about what ways people can get images.

  • Direct path - if a user knows the path to an image, they can simply go straight to it, and download the image.
  • Drag and drop - in most browsers today, you can simply drag and drop an image right onto your desktop.
  • Hotlinking - another website can link directly to your image
  • Screenshot - a visitor can simply take a screenshot of your page and crop the image.

How to fix them

So, now that we've established where our problems are, how can we fix them?

  • Direct path - we're going to create a PHP script to "hide" the direct path.
  • Drag and drop - we're going to implement something that Flickr does... placing a 1x1 transparent GIF on top of the image.
  • Hotlinking - using .htaccess, we can prevent hotlinking from occurring
  • Screenshot - not much you can do here. The best recommendation I have is to add a watermark to your image (not covered in this tutorial)

Blocking Direct Path Access

To block a user from using the direct path route, we will implement a combination of PHP and .htaccess. What we will do is create a PHP file that serves as the "proxy" for the image, and prevents the user from seeing the final path. So...let's get to it.

image.php


$_GET['f'] = "protectedImages/" . $_GET['f'];
$type = getFileType($_GET['f']);
if (acceptableType($type)) {
header("Content-type: $type");

echo file_get_contents($_GET['f']);
exit;
}
header('HTTP/1.1 403 Forbidden');
exit;

We first set the image path for the image. We point it into the 'protectedImage' directory to prevent someone from browsing around. True, they could simply set ?f=../../ and get out, but that is why we use the getFileType and acceptableType functions.

getFileType()


function getFileType($file) {
//Deprecated, but still works if defined...
if (function_exists("mime_content_type"))
return mime_content_type($file);

//New way to get file type, but not supported by all yet.
else if (function_exists("finfo_open")) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $file);
finfo_close($finfo);
return $type;
}

//Otherwise...just use the file extension
else {
$types = array(
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
'gif' => 'image/gif', 'bmp' => 'image/bmp'
);
$ext = substr($file, strrpos($file, '.') + 1);
if (key_exists($ext, $types)) return $types[$ext];
return "unknown";
}
}

In this simple function, we first try using mime_content_type. This function has been deprecated, but if it's defined, we'll try to use it. Otherwise, we'll try the Fileinfo library that was added into the PHP 5.3.0 and PECL fileinfo 0.1.0. But, if that isn't available, we'll just revert to using the good ol' file extension method. Since I assume you uploaded all of the files yourself, I assume it is safe to use. You may want to alter this if users are allowed to upload their own pictures.

acceptableTypes()

Now that we have our file type, let's validate it. This will prevent users from trying to get PHP files, or any sort of file they shouldn't get. Here it is...


function acceptableType($type) {
$array = array("image/jpeg", "image/jpg", "image/png", "image/png");
if (in_array($type, $array))
return true;
return false;
}

Of course, if you want to use other image types, feel free to add them to the array. But, those are some of the common ones (sorry bmp and gif).

From there, we set our header to tell the browser what we're returning, and then send the browser the contents of the image we want to send. The exit is important because you don't want to return a 403, which is the default for a bad request.

Your image script should work just fine now. Try it out, and see how it goes. Don't forget to change the image directory if you have a folder named something other than "protectedImages".

Preventing Drag and Drop

To prevent drag and drop of images, we're going to use a little trick that I noticed Flickr was using. They simply stretch a 1x1 transparent gif over the image, so when a user either tries to drag and drop (or right-click and "Save image as"), the image saved is the gif, not the actual image.

First, download a 1x1 transparent gif here.

Now, in your HTML, all you will need to do is add in the image. So, let's do that first. Here is a pretty simple html page we'll work with.


<html>
<head>
<title>Page Title</title>
</head>
<body>
<div class="image">
<img src="image.php?f=image.jpg" alt="Image" />
<div class="cover"><img src="imageCover.gif" alt="" /></div>
</div>
</body>
</html>

You'll see that we added an image tag that uses the image.php file we just created. Of course, we will need to add some css to make this all work correctly though. Here's how to do it:


.image {
overflow: hidden;
position: relative;
float: left;
}
.image .cover, .image .cover img {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}

So, what's this do? The wrapping div (.image) is given a position: relative to allow us to absolute position the cover image inside this div. Otherwise, the cover image would be positioned to the browser's top-left corner (unless another div had position:relative). The float:left and overflow:hidden forces the div to be wrapped only around the content of the div, not the entire width of the browser. This also ensures that the image cover, when given 100% width uses the width of the div, not the window.

So, test it out and see what happens! The cover image appears on top, and if you try to right-click and save, it grabs the transparent image, not the real image. Then, try to drag and drop it onto your desktop. Same thing!

Disabling hotlinking

In this scenario, we are forcing PHP to handle the image handling. So, we can technically deny ALL requests for images within the protectedImages directory. How do we do that? Create a .htaccess file within the protectedImages folder, and paste this code into it.


#Prevent directory listing
Options -Indexes

#Prevent images from being viewed
<Files *>
deny from all
</Files>

The first line prevents any sort of directory listings. Some servers do this automatically, but others may not. Without this, a user may be able to see a directory listing of all files. The second set of parameters tells the server to deny any and all files. If anyone tries to visit the page, they will get a "Forbidden" message.

Further protection on the image.php file

Although we have the image.php file handling all of the images, it really isn't protected. Try inserting the URL for the image src directly into your browser. You'll get an image, and you can download it. Let's do a little trick to prevent that. What we'll do is two things: force the image.php to be referred by the site and allow the user to get images only within two seconds of visiting a page.

Forcing the referral

When a browser visits your index.html page, any images that are requested will have a referrer of your page. So, we can use that. We'll use .htaccess to handle that!


RewriteEngine on
RewriteCond %{HTTP_REFERER} ^$
RewriteCond %{SCRIPT_FILENAME} image\.php
RewriteRule (.*) image.php?onlyHappensFromHTACCESS=denied [QSA,L]

This uses the RewriteEngine module in Apache servers. It has two different conditions. It first ensures that the referrer is not empty, and checks that the page being requested is the image.php script. If both of these are true, we'll redirect the request to go to image.php?onlyHappensFromHTACCESS=denied. What we'll do is display a different message to the user.

In the image.php file, we need to add a check for this. Around your previous code, wrap a check for $_GET['onlyHappensFromHTACCESS']. Here's what it looks like:


if (!isset($_GET['onlyHappensFromHTACCESS'])) {
//Our previous code here
}

Since our page exits every time if we run our own code, the 'else' for the above statement can be a plain HTML page. Here's what I use.


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>Image Denied</title>
<style type="text/css" media="screen">
body {
background-color: #ccc;
font-family: Helvetica, Arial;
}
#wrapper {
margin: 30px auto;
background-color: #ffffff;
-moz-border-radius: 15px;
-webkit-border-radius: 15px;
border-radius: 15px;
width: 800px;
padding: 20px;
}
</style>
</head>

<div id="wrapper">
<h3>Access Denied!</h3>
<p>You have tried to access an image, but due to security reasons, you cannot view the image.</p>

<p>If you wish to use the image you requested, please contact me.</p>
</div>
</html>

Session timer

We'll also add in a two-second "timer" to ensure the user visited one of our pages first.

In each page you are going to use (index.php in our case), add a simple session variable we can check. Here's an example:


session_start();
$_SESSION['lastcheck'] = time();

Then, in our image.php, we need to check this variable, and see if it's within our allotted time frame. Here's what I have:


function goodTiming() {
$n = time();
session_start();
if ($n - $_SESSION['lastcheck'] > 2 )
return false;
return true;
}

Of course, if you're using a session earlier, you don't need to start it in this function. Now, the final step is to add it into the image.php checks.


if (!isset($_GET['onlyHappensFromHTACCESS'])) {
$_GET['f'] = "pictures/" . $_GET['f'];
$type = getFileType($_GET['f']);
if (acceptableType($type)) {
if (goodTiming()) {
header("Content-type: $type");

echo file_get_contents($_GET['f']);
exit;
}
}
header('HTTP/1.1 403 Forbidden');
exit;
}

Now that we've done all of this, our images are pretty much safe. Of course, there are still ways to get it, but there is no 100% way to safeguard your images. If you've got it on the internet, it can be retrieved. But, this will at least deter the simple attempts.

Full code source

image.php


<?php
if (!isset($_GET['onlyHappensFromHTACCESS'])) {
$_GET['f'] = "protectedImages/" . $_GET['f'];
$type = getFileType($_GET['f']);
if (acceptableType($type)) {
if (goodTiming()) {
header("Content-type: $type");

echo file_get_contents($_GET['f']);
exit;
}
}
header('HTTP/1.1 403 Forbidden');
exit;
}

function getFileType($file) {
if (function_exists("mime_content_type"))
return mime_content_type($file);
else if (function_exists("finfo_open")) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $file);
finfo_close($finfo);
return $type;
}
else {
$types = array(
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
'gif' => 'image/gif', 'bmp' => 'image/bmp'
);
$ext = substr($file, strrpos($file, '.') + 1);
if (key_exists($ext, $types)) return $types[$ext];
return "unknown";
}
}

function acceptableType($type) {
$array = array("image/jpeg", "image/jpg", "image/png", "image/png");
if (in_array($type, $array))
return true;
return false;
}

function goodTiming() {
$n = time();
session_start();
if ($n - $_SESSION['lastcheck'] > 2 )
return false;
return true;
}

?><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>Image Denied</title>
<style type="text/css" media="screen">
body {
background-color: #ccc;
font-family: Helvetica, Arial;
}
#wrapper {
margin: 30px auto;
background-color: #ffffff;
-moz-border-radius: 15px;
-webkit-border-radius: 15px;
border-radius: 15px;
width: 800px;
padding: 20px;
}
</style>
</head>

<div id="wrapper">
<h3>Access Denied!</h3>
<p>You have tried to access an image, but due to security reasons, you cannot view the image.</p>

<p>If you wish to use the image you requested, please contact me.</p>
</div>
</html>

index.php


<?php session_start(); $_SESSION['lastcheck'] = time(); ?>
<html>
<head>
<title>Page Title</title>
<style type="text/css">
.image {
overflow: hidden;
position: relative;
float: left;
}
.image .cover, .image .cover img {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="image">
<img src="image.php?f=image.jpg" alt="Image" />
<div class="cover"><img src="imageCover.gif" alt="" /></div>
</div>
</body>
</html>

.htaccess in main folder


RewriteEngine on
RewriteCond %{HTTP_REFERER} ^$
RewriteCond %{SCRIPT_FILENAME} image\.php
RewriteRule (.*) image.php?onlyHappensFromHTACCESS=denied [QSA,L]

.htaccess in protectedImages folder


#Prevent directory listing
Options -Indexes

#Prevent images from being viewed
<Files *>
deny from all
</Files>

Feel free to post comments!