Watermarking hotlinked images

Bandwidth is cheap, but missing attribution is expensive.

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 hotlinked file. 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 preferred 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; if yours doesn't, consider switching to Dreamhost)
  • Access to mod_rewrite or equivalent functionality (less common, but again, most decent webhosts provide this)

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)

add-watermark.php

<?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) < 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.

Also, 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 (usually setting it world-writable; good webhosts don't require that you do this, so again, you might want to consider Dreamhost if this is the case).

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)

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:

.htaccess

RewriteEngine On

# No 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://([^/]*\.)?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://([^/]*\.)/search\?q=cache\:.*$ [NC]

# Weblog syndications
RewriteCond %{HTTP_REFERER} !^http://([^/]*\.)?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 on my site:

looks like this when it's hotlinked:

So people know where it's from while still seeing the original image. And, I get a log entry so if anyone is being excessively abusive it's easy enough to track them down and send them a polite email.

Updates

  • November 30, 2014: Cleanups
  • September 24, 2014: Rewrote this from a simple sh CGI to PHP, since PHP is more universally-available and there's some nasty security holes in some versions of bash.