Watermarking hotlinked images

What’s hotlinking?

“Hotlinking” is where you link to an image stored on someone else’s site without making your own local copy. It’s done a lot by people posting to forums, weblogs, or to social networking sites such as MySpace.

The usual solution to this problem is to redirect them to another (often tasteless) image indicating one’s dissatisfaction with the act of hotlinking. This is a pretty good disincentive from using other peoples' bandwidth; however, I care more about people maintaining attribution, and standard hotlink-prevention stuff simply just causes people to upload a copy of the image to another site and then keep on not attributing it; in this case it’s even worse because if someone were to look at the image’s URL, they don’t see anything regarding where the image originally came from.

One solution is to make a simple image proxy script which adds a watermark to an image, and also logs the hotlinked image.

Requirements

To use this, you’ll need:

  • A webhost which allows you to run PHP scripts (most do)
  • The webhost to also have ImageMagick and its PHP module (again, most do)
  • Access to mod_rewrite or equivalent functionality

Note that for Publ-based sites like this one, the easiest way to satisfy these requirements is to configure a PHP-enabled subdomain for your static content storage and set this up there.

The watermarking script

I have the following script sitting in my server root directory: (let’s call it add-watermark.php though I prefer to keep the location private)

<?php

@mkdir(".wm", 0777);

$in = preg_replace(['-^/-', '-\.\./-'], '', $_SERVER['PATH_INFO']);
$out = ".wm/$in";

$log = fopen(".wm/log-" . date('Y-m'), 'a');
fwrite($log, date(DATE_W3C) . '|' . $_SERVER['PATH_INFO']
       . '|' . $_SERVER['HTTP_REFERER'] . "\n");
fclose($log);

if (file_exists($in)
    && (!file_exists($out) || filemtime($out) &lt; filemtime($in))) {
    $image = new Imagick();
    $image->readImage($in) or die("Couldn't load $in");

    $wm = new Imagick();
    $wm->readImage("watermark.png") or die("Couldn't load $wm");

    $image->compositeImage($wm, imagick::COMPOSITE_OVER, 0, 0);

    @mkdir(dirname($out), 0777, true);
    $image->writeImage($out);
}

header('Location: /' . $out);
?>

This script sanitizes the URL, composes the watermark image on top of the requested image, and caches the result for later, and redirects the browser to it (via a 302 redirect, so the original URL doesn’t get the watermarked image in its cache entry). It also logs the hotlink so you can check up on frequent offenders.

Note that it doesn’t currently handle URL-encoded characters, so spaces in the filename and so on will probably break this.

Finally, depending on how your server’s PHP security is set up, you might need to create the .wm directory yourself and change the permissions such that the web server can write to it.

The watermark image

The script above needs an image to work on. I keep this image in the same directory as the watermarking script: (the purple represents transparency; the actual watermark file is a transparent PNG and not the flattened JPEG you see here)

It’s simple and to the point.

Proxying the images

Next we use the magic of mod_rewrite. It helps to understand regular expressions; at the very least change the example\.com to your own domain, though (and note that the \ before the . is purposeful and you should keep it in):

RewriteEngine On

# Lack of referrer is okay
RewriteCond %{HTTP_REFERER} !^$ [NC]
# Avoid an infinite loop
RewriteCond %{REQUEST_URI} !\.wm/.* [NC]
RewriteCond %{REQUEST_URI} !/add-watermark.php/.* [NC]
# Don't watermark it if it's being shown on this site
RewriteCond %{HTTP_REFERER} !^(http|https)://([^/]*\.)?example\.com($|/.*) [NC]
# Things in the /stuff directory are okay to be hotlinked
RewriteCond %{REQUEST_URI} !^/stuff/ [NC]

### Sites to not watermark
# Let's be friendly to search engine image caches
RewriteCond %{HTTP_REFERER} !^(http|https)://([^/]*\.)/search\?q=cache\:.*$ [NC]

# Weblog syndications
RewriteCond %{HTTP_REFERER} !^(http|https)://([^/]*\.)?bloglines.com($|/) [NC]
# (other whitelisted regular expressions go here - start them with ! to negate them)

# If something gets this far, it's hotlinked and not whitelisted; add the watermark
RewriteRule ^(.*)/([^/]*\.(gif|png|jpg)) /add-watermark.php/$1/$2 [R,L]

And there you have it; an image which looks like this normally:

looks like this when on a third-party site:

So people know where it’s from while still seeing the original image. And, the log entries can help you track down someone who is being particularly abusive.

Updates

  • November 30, 2014: Cleanups
  • September 24, 2014: Rewrote this from a simple sh CGI to PHP, since PHP is more universally-available and performant, and there’s some nasty security issues in some versions of bash.
  • May 27, 2018: This site doesn’t use PHP anymore so this is only here for other peoples' reference.

Comments