"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!


Dave's picture
Mar 26, 2010
4:28 pm
Dave

Thanks for your interesting post, I´ve just diabled hotlinking and tried some other things as it takes a lot of time to get a real good photograph, and I want to show it on the internet, but I don´t want otherst to use ist for their one pages.

Henk's picture
Apr 6, 2010
4:43 am
Henk

Hello,

I am testing the source on my ISP server Apache.
Work ferry nice and good.
Now i want to test it on my localhost XAMPP Apache server at home.
This dont work, i think the problem is in htaccess??
I have rewrite on and all for overwrite.
Can you help me?

Regards Henk from the Netherlands.

mikesir87's picture
Apr 6, 2010
7:33 am
mikesir87

Are you sure you've got mod_rewrite enabled on your XAMPP server? If you just did it, make sure to restart the server, and then try again.

Try this link and see if it helps out:
http://www.lancelhoff.com/enabling-htaccess-in-apache-on-windows/

Henk's picture
Apr 6, 2010
3:35 pm
Henk

Dear Mikesir87,

Thanks for your reaction,
I have found why it dont work on XAMPP.
Its in de image.php source.
I have marked oud this function
[code]
if (acceptableType($type)) {
xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxx
} [/code]
and yes it works fine.
I wil try to find out why?

Thanks and regards Henk...

mikesir87's picture
Apr 6, 2010
5:15 pm
mikesir87

What kind of image are you trying to use? (jpg, gif, png, etc.) Check what the $type variable is set to, and let me know what that is.

Fraso's picture
Apr 7, 2010
7:49 pm
Fraso

Does this stop people from saving the webpage to their computer and then going through the files that come with the html page? Or using firefox's nifty media browser?

Obviously these things will stop regular users from getting your images, but I always thought the only way to really protect the source image was using flash... and if they are really determined (and don't care about the quality) they could always SS that.

I saw your "If you've got it on the internet, it can be retrieved. But, this will at least deter the simple attempts." disclaimer, so obviously tech-savvy people are going to be the ones getting around it. Would it be too much of a pain to put up a demo (or if you know of a site that uses all of these) I wanna try to break it in as many ways as I can. =D

Not the theme of the post, but you could always have copyright law on your side... if it's your picture... published works are automatically copyright.

mikesir87's picture
Apr 8, 2010
5:20 pm
mikesir87

@Fraso

I added a demo...and you are correct. There are still ways to get the image. This just simply blocks the "typical" user from figuring out how to get it. Maybe I'll do a post on how to add a watermark or something to the image.

Have fun with trying to break it, and let me know what you find!

Christian Voigt's picture
May 25, 2010
3:47 pm
Christian Voigt

Turn off css (for FF : View->Page Style->no style), right click, save image...
or left click on the little area left of your adress bar, click the media-tab and save the image.

Thats just two ways to do it in FF.

not much to it i'm afraid :)

If you really want to prevent the public from downloading your pictures, don't make them public... jumping through hoops for something you can break with four mouse-clicks is really not worth all that work.

mikesir87's picture
May 25, 2010
3:52 pm
mikesir87

Yeah... you're right. But, this will stop those that aren't really the developers and know the hoops you can go through... which accounts for a pretty good majority of the normal internet users.

I do agree with you though... if you don't want people to get it, don't make it public.

Christian Voigt's picture
May 25, 2010
4:24 pm
Christian Voigt

Two years ago, i would have agreed. I would still agree, but things are happening so fast. The six year old son of a friend of mine wanted to save a protected picture, fiddled with it for a few seconds and then googled, "download protected picture". One of the first four hits took him to a tutorial on how do do it fast and easy. (just googled it myself, very nice tutorials on how to break any kind of protection)

So i agree that you can protect your pics from 100% noncoms... but what would they do with it anyway. And everyone remotely familiar with google will have no trouble at all.

I would recommend protecting your pictures from hotlinking, that is still a useful technique and will prevent hijacking.

"normal" internet users are a dying breed. Even my mum is ripping mp3's from online flash-players, after googling for a few minutes on how to do it. She calls on me to update her Antivirus-Software, but she knows how to get what she wants if she sets her mind to it :)

It's a nice approach though, good stuff for a tutorial, i am sure many people can learn a thing or two from your code.

Anonymous's picture
Jul 27, 2010
10:30 pm
Anonymous

Instead of using an tag for the image use a div with a CSS background. This way, if they disable the style the won't see an image.

With the fix, it would prevent everything but screen capture. Now, I just need to implement this using a jQuery gallery. Any thoughts?

Christian Voigt 's picture
Jul 28, 2010
4:42 am
Christian Voigt

You'd still have it pretty easy with integrated media-browsers. Like the aforementioned Media-Tab in Firefox for Example.

A way to really inconvenience people would be to slice the image to chunks and rebuild them on display. They could still download the chunks, but then have to start up some image editor to piece the puzzle together. Or go with the screen-grabber option after all...

I am sure some scripts would help on the developer side, like automatic splitting and generating needed html/css fragments for display...

But as always, I haven't put much thought to this idea, and I still doubt that any effort to protect public images any other way than with a simple copyright notice is just too much work to be economically viable.

mikesir87's picture
Jul 28, 2010
6:43 pm
mikesir87

I agree with the things Christian said. To really protect images, the only thing you can really do is watermark it. There's just too many tools out there to scrap them (or screen capture them).

However, I do like the idea of a image-slicing tool. Might be worth developing at some point... ??

Jason's picture
Jul 28, 2010
6:56 pm
Jason

For the average viewer it will be tough but still possible for programmers like us. I think smugmug has the best one out there. You really have to sift though using firebug in order to find the image but alas it can be found.

I think the best way is going to use a flash gallery. This will give the only option of screen capture. I really don't want to rack my brain about trying to protect images anymore, it just can't be done. Even some watermarks can be erased using photoshop. As a joke, my friend un watermarked one of my photos and sent it back to me with his watermark!!

Bob Holloran's picture
Aug 24, 2010
7:01 pm
Bob Holloran

The jpg and pdf images that I need to protect are only accessible from a password protected php/MySQL driven website. The only vulnerability of concern is the direct downloading using the URL. So, most of the code listed is not necessary. However, several of my php templates need to be able to access the files in the protected directory based upon SQL queries of the database that provide the file name of the image/pdf to be rendered. I have also tried locating the files in directories outside the html root directory, but my php scripts have not been able to render the images located in these directories. I am able to upload both jpg and pdf files and relocate them to the directories outside the html root directory, as well as to .htaccess protected directories.

What parts of your code do I need to use and where would they go?

mikesir87's picture
Aug 24, 2010
7:20 pm
mikesir87

Bob,

If the images are accessible when only logged in, you can add whatever checks you want at the beginning of the image.php script I have above. You can check if they are logged in, if they have the correct access privileges, etc.

As far as why your php scripts have not been able to render the images in your directories, I would first check permissions on the directories. I would assume they are right since you can write to it, but still worth a check on the read ability. Your php scripts won't be affected by your htaccess protection, so should be able to simply use file_get_contents, and echo that out (after setting your headers appropriately).

As I'm not 100% sure what errors you're getting, feel free to send a contact me, and we can start corresponding via email. I would love to help out.

Sunil's picture
Dec 21, 2010
2:58 am
Sunil

Thanks...........
This is an awesome article.
And i learnt a lot from this article.
Keep it up!!!
Once again thanks for sharing your knowledge...

Anonymous's picture
Jan 25, 2011
7:42 am
Anonymous

Try this... PrintScreen :-] PWNED! ;-)

Ajit Kumar Singh's picture
Feb 22, 2011
2:44 pm
Ajit Kumar Singh

I think its a interesting article, But there be a small problem on it.
By using firfox >> Go to source file >> save >> image.php

Now just rename the file image.php to image.jpg and you will able to view.

Anonymous's picture
Mar 8, 2011
12:07 pm
Anonymous

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

Anonymous's picture
Apr 16, 2011
6:54 am
Anonymous

Hi your code was really excellent. Finally I decided to use your code in my project. But will definitely share the code if any thing amended.

Anonymous's picture
Jun 4, 2011
12:33 am
Anonymous

Hi. I'm using jquery prettyphoto image gallery to display my images. When u click on the image on a page it will bring a modal window which shows the image and u can click next or prev to view other pic. So will this .htaccess works (which prevent Forcing the referral)? That modal window I believe it's just something created from ccs and jquery so will it still have referral? Hope u can let me know what u think. Thank you.

Pradeep Singh's picture
Jul 23, 2011
6:14 am
Pradeep Singh

Hi,

I am able to save this image very easly in IE8 browser.

Can you please check and provide more secure solution.

Thank You,
Pradeep K Singh

Anonymous's picture
Sep 19, 2011
11:13 am
Anonymous

do you have a solution for Wordpress?

Detlev's picture
Dec 26, 2011
2:37 am
Detlev

I don't recommend using this method. This entire download-prevention is based on a single CSS div overlaying on top of the image. Disable styling in your browser and bye-bye security.

Tobias Beuving's picture
Jan 29, 2012
5:51 am
Tobias Beuving

Dear Michael,

In the code examples appear breaks and paragraph tags, might be a little confusing for novice coders. Probably easily fixable.
Great article though - I like the 'onlyHappensFromHtAccess' trick! :)

Cheers,

Tobias

Post new comment

The content of this field is kept private and will not be shown publicly. If you have a Gravatar account associated with the e-mail address you provide, it will be used to display your avatar.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
  • Syntax highlight code surrounded by the {syntaxhighlighter SPEC}...{/syntaxhighlighter} tags, where SPEC is a Syntaxhighlighter options string or "class="OPTIONS" title="the title".

More information about formatting options