Valid XHTML Icon Valid CSS Icon Valid SVG Icon Unicode UTF-8 Icon
Add to del.icio.us Digg this! Post to reddit Share on Facebook Add to StumbleUpon Add to Google Bookmarks

Stop Hotlinking

Creative Commons LogoThis work is licensed under the Creative Commons Attribution Non-Commercial 2.0 UK: England & Wales Licence. This means that you are free to: copy; distribute; and modify this work. It also means that you cannot use it for commercial purposes. Additionally, you must attribute this work to the original author, Thomas Guymer, ideally with a link.

First, you may ask What is hotlinking? Hotlinking is when someone uses one of your images on their webpage. Usually this is very bad for two reasons:

It is this second point which is 'hotlinking'. Below are two examples of someone hotlinking my images. They are real examples and if you click on the previews you'll get the full-sized image and so you can read the address bar and check them out if you want.

This article will show you how to stop the hotlinking of your images. This article has been written after I read a great article on the subject at A List Apart (called Smarter Image Hotlinking Prevention) and I felt I could improve on it by simplifying the solution.

Firstly, one must edit the .htaccess file on your webserver. Now this is the solution that a quick Google search will turn up, but once this solution is implemented we have not finished, there is a final touch which needs to be done. So, in the root of your webserver create a file called .htaccess which contains these lines:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} .*jpg$|.*jpeg$|.*bmp$|.*gif$|.*png$ [NC]
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http://(.+\.)?exampledomain1 [NC]
RewriteCond %{HTTP_REFERER} !^http://(.+\.)?exampledomain2 [NC]
RewriteRule (.*) /path/to/custom/script.php?src=$1

If the file already exists then just append these lines. If you are having difficulty creating this file then call it something else and edit it on your computer then upload it via FTP and rename it once it is on the server.

What this file does is only look at files which have extentions jpg, gif or png. Then, when someone tries to access one of these files it looks to see where the request is coming from. If the request is from one of your 'example domains' (which you can change to your own website), or if the HTTP Referer is empty, then the request can procede as normal. If not, then the request will get sent to /path/to/custom/script.php?src=/path/to/requested/image.jpg. Now, the user will not see this address, they will get the address that they want but the actual place where the code came from will be that PHP script. As far as the user is concerned this is a totally transparent process - they will never know.

Now we need to create that PHP script and then we're done. You'll have to make sure that the link in the .htaccess points to the actual location of the PHP script we're about to make. The PHP script should contain something like:

<?php
header("Content-type: text/html");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Random Date in Past
header("Cache-Control: no-cache");
header("Pragma: no-cache");
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Hotlinked Image</title>
    <meta name="description" content="A page to display an image as it was directly linked to by an external site." />
</head>
<body>
<h1>Requested Image</h1>
<?php
if(!isset($_GET["src"]) || empty($_GET["src"])) {
    echo "<p>No image was linked, perhaps you've just navigated to this page on your own?</p>";
}
else {
    echo "<p>As you clicked on a <i>direct</i> link to this image from an external site, this page has been shown. The image you're after is shown below. You may click on it to fully display just the image. You may have to press <i>F5</i> to refresh it once you've clicked on the link.</p>";
    $temp = getimagesize($_SERVER["DOCUMENT_ROOT"] . "/" . strip_tags($_GET["src"]));
    $desiredWidth = 600;
    if($temp[0] > $desiredWidth) {
        $width = $desiredWidth;
        $height = $temp[1] * ($desiredWidth/$temp[0]);
    }
    else {
        $width = $temp[0];
        $height = $temp[1];
    }
    echo "<p><a href=\"/" . strip_tags($_GET["src"]) . "\" title=\"Requested Image\"><img src=\"/" . strip_tags($_GET["src"]) . "\" alt=\"Requested Image\" width=\"" . $width . "\" height=\"" . $height . "\" /></a></p>";
    unset($temp,$desiredWidth,$width,$height);
}
?>
</body>
</html>

Now, a few things need to be explained so you can see how it works.

  1. The header("Content-type: text/html"); is very important. This tells the web browser that this file is a HTML file. This will stop the image being displayed straight away as the browser is expecting an image, so to be told it's getting some text will throw a spanner in the works. Problem solved: image is not displayed.

  2. Now, the stuff about header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); is just telling the browser not to remember this page so that if it visits it again it'll fetch it without using a cached version.

  3. Now, the rest is just a fancy way of displaying the image that the person wanted if they visit the page. This will only happen if the user visits the page on its own and it's just a way to show your content, on your site. It contains a link to see the image fullsize. Now obviously this will have the same address, but as they will have clicked on a link from your own site then they'll just get the image on it's own. This is also why we put the anti-cache lines in as we don't want the browser to remember the text bits.

The reason why this method should be used (and is better) than simple denials is that if someone puts a text link to your image on a website and then a user follows this link it will look to the .htaccess file as if it is being hotlinked and it will not display it - which is bad! Using this method stops your images being displayed on alien websites but still shows your image for visitors coming from those alien websites of their own accord.

To continue the worked example, this is what their pages look like after my solution has been implemented:

...you see? No images! If a link was provided to one of the images then the HTTP Referer will also be the same as a hotlink and so it will trigger the PHP page, as we've already discussed above. The pages will then look like this:

...which show the preview image and a link to see the image on its own. I hope this helped!

This page was last modified on 20/09/2015.

The Unfriendly Web

Map of total firewall denials on my servers

Update (September 2015)

This site has changed quite a lot since I bought the domain in November 2005 and first started hosting my own content. During that time the internet has evolved an awful lot; as I write this in 2015 it is almost unrecognisable to what it once was. Gone are the days of hosting your own photo albums from your holidays on your own website: now you create an album on Facebook to share with your friends and family. Got some special photos that you are particularly proud of? Then deviantART or Flickr are the places for you to showcase them. Found an interesting page and wish to share it with your friends? Twitter and Facebook will update them immediately wherever they are. Written some pieces of source code that you think other people might find useful? GitHub will version track and syntax highlight it in an instant.

Consequently, this site no longer has photo albums and panoramas taken from my travels: the special ones are in my deviantART gallery and the normal ones are on my private Facebook page. I don't have a WordPress blog at the minute so I will still keep my articles on fixing technological problems ("Releases" and "Tutorials") on here for archival - if they're useful to you then that's great. If I ever restart publishing code it'll be on my GitHub page.

I have learned a lot since I first started writing (non-public) web pages in 2002. As testimony, this site: does not set any cookies; barely has any JavaScript on it; and is no longer dynamically generated using PHP. Rather, it is completely static with updates propagated using make every midnight thanks to cron.

© 2002-2017 Thomas Guymer. See the Copyright Statement.