← News
/PLAYBOOK · 2026-09-05

Nginx Hotlink Protection: valid_referers and secure_link Against Resource Theft

Hotlinked images, video, and downloads burn bandwidth. Two Nginx anti-hotlink designs: valid_referers source checks and secure_link time-limited signed URLs. Full config, curl verification, and false-positive troubleshooting for static-heavy sites.

Images, video, and download files referenced directly by third-party sites consume origin bandwidth for free. That is classic hotlinking. This article covers two core Nginx anti-hotlink designs: valid_referers source checks and the secure_link module for time-limited signed URLs. Full config, verification, and troubleshooting. Aimed at static-heavy sites.

Applicable Scenarios

Prerequisites

How It Works

Anti-hotlink is verifying that the request comes from an authorized context. Nginx provides two layers:

Option 1: Referer check (`valid_referers`). Browsers send a Referer header identifying the source page. Nginx declares legal sources with `valid_referers`; requests outside the allowlist set `$invalid_referer` to 1, and `if` returns 403. Simple to deploy. Referer can be forged. Baseline protection only.

Option 2: Signed URL (`secure_link`). Backend concatenates resource path, secret, and timestamp per a shared algorithm and emits an MD5 signature. The client must present that signature. Nginx recomputes with the same secret and compares. Mismatch or expiry is denied. Signatures cannot be forged. Strict anti-leech. Fits VOD, downloads, and other high-value assets.

Procedure

Option 1: valid_referers

Edit the server block. Add Referer checks in the static location:

server {
    listen 80;
    server_name example.com;

    location ~* \.(jpg|jpeg|png|gif|mp4|zip)$ {
        valid_referers none blocked server_names
            *.example.com example.com
            *.baidu.com *.bing.com;

        if ($invalid_referer) {
            return 403;
        }

        root /data/www/static;
        expires 7d;
    }
}

Parameters: none allows requests with no Referer (address-bar open, in-app WebView are common); blocked allows requests whose Referer was stripped by a firewall or proxy; server_names allows this server's server_name; wildcard `*.example.com` covers all subdomains. Whether to allow search-engine domains is a business call; allowing them avoids broken images in search results.

Option 2: secure_link signed URLs

Step 1. Define signature verification in Nginx:

location /download/ {
    secure_link $arg_md5,$arg_expires;
    secure_link_md5 "$secure_link_expires$uri MY_SECRET_KEY";

    if ($secure_link = "") {
        return 403;   # missing or mismatched signature
    }
    if ($secure_link = "0") {
        return 410;   # signature valid but expired
    }

    alias /data/www/files/;
}

Step 2. Backend generates the signed link. PHP example:

<?php
$secret = 'MY_SECRET_KEY';
$expires = time() + 300;                 // 5 minutes validity
$uri = '/download/report-2026.zip';
$md5 = base64_encode(md5($expires . $uri . $secret, true));
$md5 = strtr($md5, '+/', '-_');
$md5 = rtrim($md5, '=');
$url = $uri . '?md5=' . $md5 . '&expires=' . $expires;
echo 'https://example.com' . $url;

Key points: `secure_link_md5` concatenation order must match the generator exactly. Signature uses base64 URL variant (`-_` replaces `+ /`); trailing `=` stripped. `$uri` is the decoded request path; if the path has Chinese or spaces, confirm encoding matches on both ends.

Verification

Option 1: `nginx -t` then `nginx -s reload`. Simulate sources:

curl -I -H "Referer: https://evil.com/" https://example.com/demo.jpg
# expect 403

curl -I https://example.com/demo.jpg
# expect 200 (none allows empty Referer)

Option 2: correct signature returns 200; flip any md5 character returns 403; set expires to a past timestamp returns 410. Wrap the checks in a script and cron them so a later config change cannot silently break anti-hotlink.

FAQ

FAQ 1: After Referer checks, images fail to load in WeChat and some apps?

Some clients send no Referer. If the config omits `none`, those requests are false-positived. Keep `none blocked` in valid_referers, or switch to secure_link for precise control.

FAQ 2: secure_link always returns 403. How to debug?

90% is concatenation order or encoding. Check order: `secure_link_md5` variable order matches the generator; secret is identical on both ends (watch trailing spaces); base64 used raw output plus URL-safe replace; set error_log to `notice` to see the secure_link module verification path.

Summary

valid_referers is a low-cost first line for site-wide static assets; it can be forged. secure_link enforces strict anti-leech with time-limited signatures; use it for downloads, VOD, and other core assets. Combine for layered protection. After rollout, watch access logs for false positives and tighten the allowlist gradually.