Skip to main content
Modern hotlink protection strategies — CDN-level token signing and referer checking for web assets

Modern Hotlink Protection: Strategies That Still Work

Hotlink protection — preventing other sites from embedding your images, videos, or other assets directly — remains a legitimate concern for any site serving media that costs money to deliver. The old Apache mod_rewrite referer check still appears in countless tutorials, but the web has changed around it: HTTPS referrer stripping, CDN architecture, SPA navigations that omit the Referer header, and the rise of Referrer-Policy as a first-class HTTP header have all eroded the reliability of referer-based approaches. This page sits within the web development section alongside the Apache mod_brotli guide and the defacing content scrapers entry, and connects to the web performance topic hub where bandwidth cost is the underlying concern. The sections below cover what still works, what does not, and the layered approach that provides meaningful protection without breaking legitimate use cases.


The short version

→ Short Answer

Referer-based hotlink protection is unreliable on its own in 2024 because HTTPS pages omit the Referer header when linking to HTTP resources, many browsers apply strict referrer policies that strip or omit the header, and CDNs often normalise or strip referrer information before it reaches your origin. Token-based URL signing — generating time-limited signed URLs for assets — is the only approach that provides genuine enforcement. CDN-level enforcement (Cloudflare, AWS CloudFront signed URLs, Fastly token authentication) is the practical implementation for most sites. Pure referer checking is best treated as a lightweight deterrent against casual hotlinking, not a security control.


Why the classic approach worked — and when it stopped

The classic Apache mod_rewrite hotlink protection pattern has been copied into server configuration guides for over twenty years:

Classic mod_rewrite hotlink protection (Apache .htaccess)
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?yourdomain\.com/ [NC]
RewriteRule \.(jpg|jpeg|png|gif|webp|svg|mp4|webm)$ - [F,NC]

This rule denies requests for image and video files where the Referer header is present but does not match your domain. The logic is: if someone has a referrer and it is not your site, they are hotlinking. Requests with no referrer (direct navigation, bookmarks, some crawlers) are allowed through by the !^$ condition.

This worked reasonably well in the mid-2000s when:

  • Most browsers sent accurate Referer headers
  • HTTP was the dominant protocol, so cross-origin referrers were preserved
  • SPAs were not common — page navigations involved real HTTP requests
  • CDNs were not in the delivery path for most origin servers
Then

2005–2014 era: The referer header was sent consistently for image loads triggered by <img> tags on HTTP pages. A hotlinker on an HTTP site embedding your HTTP images would send a referer header you could match against. The protection worked for the majority of casual hotlinking cases. Bypassing it required only stripping the referer header (trivial with curl or any download manager), but casual hotlinkers did not do this.

Now

Current state: HTTPS pages do not send the Referer header when loading sub-resources from a different origin unless the resource is also HTTPS and the site sets an explicit referrer policy permitting it. Since most sites are now HTTPS and most image hotlinking originates from HTTPS pages, the referer header is absent for a large proportion of legitimate same-site requests and cross-site hotlink requests alike. The classic rule either blocks legitimate users or lets hotlinkers through — often both simultaneously.


The referrer policy complication

Referrer-Policy is now a standard HTTP response header and a <meta> tag option that controls whether browsers send the Referer header and how much information it contains. The default policy in modern browsers is strict-origin-when-cross-origin, which means:

  • Same-origin requests: Full URL sent in Referer
  • Cross-origin HTTPS-to-HTTPS requests: Only the origin (scheme + host) sent, no path
  • Cross-origin HTTPS-to-HTTP requests: No Referer sent at all

A hotlinker on an HTTPS site embedding your HTTPS image will send Referer: https://hotlinkingsite.com/ — just the origin. Your referer check can still match against this to detect cross-origin requests, but it now cannot distinguish between a hotlinker embedding your image and a legitimate user clicking a link to your image from another site.

⚙ Compatibility Note

The Referrer-Policy: no-referrer meta tag on a page causes all sub-resource requests to omit the Referer header entirely. Sites that set this policy (for privacy reasons or because they copied a security hardening checklist) will appear to your server as direct requests with no referrer. A referer-based hotlink rule that blocks all requests with no referrer will break image loading for users who arrived via these sites. A rule that allows empty referrers (which the classic pattern does) will also allow hotlinks from these sites.


Strategies that provide real enforcement

Token-based URL signing

URL signing generates a time-limited, HMAC-secured token for each asset URL. The token encodes the expiry time and is signed with a secret key. When the server receives a request, it validates the token. Requests without a valid token, or with an expired token, receive a 403.

HMAC token generation (Python example)
import hmac
import hashlib
import time
import base64

SECRET = b'your-secret-key'

def sign_url(path: str, expiry_seconds: int = 3600) -> str:
expiry = int(time.time()) + expiry_seconds
message = f"{expiry}{path}".encode()
token = hmac.new(SECRET, message, hashlib.sha256).digest()
token_b64 = base64.urlsafe_b64encode(token).rstrip(b'=').decode()
return f"{path}?token={token_b64}&expires={expiry}"

def verify_url(path: str, token: str, expires: str) -> bool:
try:
expiry = int(expires)
if time.time() > expiry:
return False
message = f"{expiry}{path}".encode()
expected = hmac.new(SECRET, message, hashlib.sha256).digest()
expected_b64 = base64.urlsafe_b64encode(expected).rstrip(b'=').decode()
return hmac.compare_digest(token, expected_b64)
except (ValueError, TypeError):
return False

The practical challenge with self-implemented token signing is that your application must generate signed URLs for every asset reference — every <img src>, every CSS url(), every video src. This is feasible for dynamically generated pages but complex for static sites or cached content.

CDN-level signed URLs

If you are serving assets through a CDN, signed URL authentication is the correct implementation level. Each major CDN provides this:

Cloudflare: Cloudflare's signed URL feature (available on Business and Enterprise plans) allows you to require HMAC-signed tokens for specific URL patterns. The signing secret is configured in Cloudflare Workers and the validation runs at the edge before the request reaches your origin.

AWS CloudFront: CloudFront signed URLs and signed cookies allow time-limited access to specific files or path patterns. Signed cookies are particularly useful for protecting entire sections of a site (a video library, a members-only image gallery) without generating a unique URL per asset.

Fastly: Fastly's token authentication uses a similar HMAC approach configurable in VCL. Fastly's documentation is detailed and the implementation is well-tested at scale.

CDN-level token authentication flow for hotlink protection — request validation at the edge before origin

nginx auth_request for lightweight token validation

For origin servers running nginx, the auth_request module allows delegating authentication to an internal subrequest:

nginx auth_request for token validation
location /protected-assets/ {
auth_request /auth-validate;
auth_request_set $auth_status $upstream_status;

# Serve the actual file if auth passes
alias /var/www/assets/;
try_files $uri =404;
}

location = /auth-validate {
internal;
proxy_pass http://127.0.0.1:8080/validate-token;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}

The validation endpoint at localhost:8080/validate-token receives the X-Original-URI header, extracts the token from the query string, validates it, and returns 200 for valid requests or 401/403 for invalid ones. This keeps the validation logic in application code while nginx handles the request routing.


Referer checking that still adds value

Despite its limitations, referer-based checking remains useful as a first layer for specific scenarios:

Blocking known hotlinkers by domain: If you can identify specific domains that are hotlinking your content, an explicit referer block is fast, zero-maintenance, and effective for that specific offender.

Apache — block specific hotlinking domain
RewriteEngine On
RewriteCond %{HTTP_REFERER} ^https?://(www\.)?hotlinkingsite\.example\.com/ [NC]
RewriteRule \.(jpg|jpeg|png|gif|webp|mp4)$ - [F,NC]

CDN origin protection: If your CDN handles all public traffic, you can configure your origin to only accept requests from CDN IP ranges. This prevents hotlinkers from bypassing the CDN to access your origin directly (avoiding your CDN's bandwidth costs). This is IP-based, not referer-based, and is reliable.

nginx — restrict to CDN IP ranges (example)
geo $allowed_requester {
default 0;
# Cloudflare IPv4 ranges (abbreviated — use current list)
103.21.244.0/22 1;
103.22.200.0/22 1;
103.31.4.0/22 1;
# Add your CDN's current IP ranges
}

server {
location /assets/ {
if ($allowed_requester = 0) {
return 403;
}
# ... serve assets
}
}
⚠ Common Pitfall

CDN IP ranges change over time. Hardcoding them without a process to update them will eventually block legitimate CDN traffic. Use your CDN provider's dynamically updated IP list mechanism — Cloudflare provides an API endpoint for their current IP ranges, AWS publishes an IP range JSON file — rather than maintaining a static list.


What changed with the shift to HTTPS

The HTTPS transition broke referer-based hotlink protection in a specific way that is worth making explicit. Before widespread HTTPS adoption, a hotlinker on http://other-site.com/ embedding your image at http://yourdomain.com/img/photo.jpg would send Referer: http://other-site.com/page-with-hotlink. Your rule could match and block it.

After HTTPS became the norm, a hotlinker on https://other-site.com/ embedding your image at https://yourdomain.com/img/photo.jpg sends only Referer: https://other-site.com/ — the path is stripped by the browser's default referrer policy. Your rule can still match the origin and block it. So far so good.

↻ What Changed

The critical change is that many legitimate referrers are also stripped. Privacy-focused browsers, extensions that suppress referrers, sites that set strict referrer policies for legitimate reasons, and users navigating from private browsing mode all send either a stripped referrer or none at all. The population that sends no referrer is now large enough that blocking it would break normal usage. The classic trick of replacing blocked hotlinked images with a watermarked or replacement image — useful in the early days as a signal to hotlinkers — has become counterproductive because it affects users with no referrer for entirely legitimate reasons.


Practical recommendation

The layered approach that makes sense for most sites:

  1. CDN-level enforcement if you are already using a CDN — use signed URLs or signed cookies for assets that are expensive to serve or commercially sensitive
  2. nginx/Apache referer rules as a lightweight deterrent for known offenders, not as primary protection
  3. Origin IP restriction to ensure hotlinkers cannot bypass your CDN to access origin directly
  4. Monitor bandwidth by referrer in your CDN analytics to identify hotlinking at the cost reporting level before it becomes significant

Token signing is the only technically robust solution, but it requires application support for URL generation. For a static site or a simple image gallery, the cost of implementing token signing (both development time and operational complexity) may outweigh the benefit for typical hotlinking rates. Referer-based deterrence plus CDN analytics monitoring is a reasonable middle ground.