Keeping images from adorning other sites: stop hotlinking in Apache

Updated November 2026

Hotlinking is when another site puts your image in its pages with <img src="https://www.example.com/…">. Their visitors download the file from your server, so you pay for the bandwidth. Apache httpd 2.4 can refuse those requests by checking the Referer header. Below are three tested rule sets, which sites to let through, how to test, and what this protection cannot do.

Old article, new syntax. The June 2000 Apache Today article with this title (see the 2000 archive) used Apache 1.3 directives such as Order, Allow and Deny. In 2.4 those only work through the compatibility module mod_access_compat. Everything on this page uses current 2.4 syntax and was tested on Apache httpd 2.4.69, the current release as of November 2026.

How hotlinking works

When a browser loads a page, it fetches every image on it. With each image request it normally sends a Referer header (the misspelling is in the HTTP standard) saying which page asked for the image. So if a page on hotlinker.example shows your photo, your server sees:

GET /images/photo.jpg HTTP/2
Host: www.example.com
Referer: https://hotlinker.example/

Your rule compares that value with your own domains. Three cases matter: no Referer at all (serve the image), a Referer from your own site (serve it), and a Referer from any other site (refuse it, or send a stand-in image).

Browser page on hotlinker.example GET /images/photo.jpg Referer: hotlinker.example Apache httpd checks the Referer No Referer 200: serve the image Your own domain 200: serve the image Any other site 403 or stand-in image
The decision Apache makes for every image request.

Before you start

Method 1: mod_rewrite

This works everywhere and is the only method that can also send a stand-in image. In .htaccess in the document root:

RewriteEngine On
# 1. Let requests without a Referer through
RewriteCond %{HTTP_REFERER} !^$
# 2. Let your own domain and all its subdomains through
RewriteCond %{HTTP_REFERER} !^https?://([a-z0-9-]+\.)*example\.com(:[0-9]+)?(/|$) [NC]
# 3. Everything else asking for an image gets 403 Forbidden
RewriteRule \.(gif|jpe?g|png|webp|avif|svg)$ - [F,NC]

Conditions are combined with AND, so the rule fires only when there is a Referer and it is not yours. - means "do not change the URL" and [F] returns 403. [NC] makes both matches case-insensitive, so PHOTO.JPG is covered too.

The domain pattern deserves a look, because the short pattern in many tutorials (!example\.com) is easy to beat:

What the pattern accepts
RefererResult
https://example.com/, https://www.example.com/pageAllowed
https://cdn.img.example.com/x, http://example.com:8080/Allowed
https://example.com.hotlinker.example/Blocked (the (/|$) ending stops this trick)
https://myexample.com/Blocked (subdomains must end in a dot)

In the virtual host, put the same four lines inside <VirtualHost *:443>. Rewrite rules are not inherited by virtual hosts, so each vhost (HTTP and HTTPS) needs its own RewriteEngine On. The RewriteRule pattern above has no ^/, so it works unchanged in both places.

Several sites of your own? List them in one group: !^https?://([a-z0-9-]+\.)*(example\.com|example\.net|example-shop\.org)(:[0-9]+)?(/|$).

Show a stand-in image instead

Instead of a broken image, the hotlinker's page can show a small "image from example.com" graphic. Rewrite internally, and exclude the stand-in itself so the rule cannot loop:

RewriteEngine On
RewriteCond %{REQUEST_URI} !^/images/no-hotlinking\.png$
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://([a-z0-9-]+\.)*example\.com(:[0-9]+)?(/|$) [NC]
RewriteRule \.(gif|jpe?g|png|webp|avif|svg)$ /images/no-hotlinking.png [NC,END]

# Keep caches from storing the stand-in under the real image's URL
<Files "no-hotlinking.png">
    Header set Cache-Control "no-store"
</Files>

An internal rewrite costs one request instead of two. If you use an external redirect ([R=302,L]) instead, the exclusion line is essential: the browser repeats the hotlinker's Referer when it follows the redirect. Apache adds Vary: Referer to the stand-in response, but a cache in front of your site that ignores Vary could show the stand-in to your own visitors, hence the no-store header. If in doubt, plain 403 is simpler and safer.

Method 2: expressions with Require

Without mod_rewrite, an <If> block can deny the request through normal access control:

<FilesMatch "\.(?i:gif|jpe?g|png|webp|avif|svg)$">
    <If "-n %{HTTP_REFERER} && %{HTTP_REFERER} !~ m#^https?://([a-z0-9-]+\.)*example\.com(:[0-9]+)?(/|$)#i">
        Require all denied
    </If>
</FilesMatch>

-n means "is not empty", !~ means "does not match", and the trailing i makes the match case-insensitive. A one-line version with the same effect is Require expr "-z %{HTTP_REFERER} || %{HTTP_REFERER} =~ m#…#i" inside the <FilesMatch>. In the server configuration, wrap either one in <Directory "/var/www/example.com/public">. Note that a Require here replaces any other Require rules inherited for those files, such as an IP restriction; combine them with <RequireAll> if you need both (Access control).

Method 3: SetEnvIf and Require env

This is the approach the Apache documentation itself recommends for simple cases. Tag good requests with an environment variable, then require it:

SetEnvIf Referer "^$" allowed_referer
SetEnvIfNoCase Referer "^https?://([a-z0-9-]+\.)*example\.com(:[0-9]+)?(/|$)" allowed_referer
<FilesMatch "\.(?i:gif|jpe?g|png|webp|avif|svg)$">
    Require env allowed_referer
</FilesMatch>

"^$" matches both a missing and an empty Referer. The old 2.2 syntax you may still see does the same with Order Deny,Allow, Deny from all and Allow from env=allowed_referer; replace it with Require env when you upgrade.

Which method to use

Method.htaccess needsStand-in imageNotes
mod_rewriteFileInfo + FollowSymLinksYesMost flexible; add exceptions with more RewriteCond lines
<If> / Require exprAuthConfigNoReadable; adds Vary: Referer to served images
SetEnvIf + Require envFileInfo + AuthConfigNoSimplest; easy to add allowed sites, one line each

Why allow an empty Referer

Many legitimate requests carry no Referer: someone opens the image URL directly or from a bookmark, a privacy extension strips the header, a feed reader or mail program fetches the image, and link-preview services and search engine crawlers download images from their own servers. Blocking them breaks your own visitors and your previews.

Browsers also limit what they send. Since the default Referrer-Policy became strict-origin-when-cross-origin, a page on another site sends only its origin (https://hotlinker.example/), not the full page address, and sends nothing at all when an HTTPS page loads an image over plain HTTP. Two consequences:

Your own pages are fine: same-origin requests send the full URL, and images on a subdomain such as img.example.com receive https://www.example.com/, which the pattern allows. If you set Referrer-Policy: no-referrer on your own site, your images arrive with no Referer, so never remove the empty-Referer exception in that case.

Sites you may want to allow

Test it with curl

Test from the command line, not the browser, because the browser caches images. -e sets the Referer (same as -H "Referer: …") and -I asks for headers only:

curl -sI -e https://hotlinker.example/ https://www.example.com/images/photo.jpg | head -1
HTTP/2 403

curl -sI -e https://www.example.com/blog/ https://www.example.com/images/photo.jpg | head -1
HTTP/2 200

curl -sI https://www.example.com/images/photo.jpg | head -1
HTTP/2 200

Also try the tricks the pattern should catch, such as -e https://example.com.hotlinker.example/. After editing the server configuration, check and reload: sudo apache2ctl configtest && sudo systemctl reload apache2 on Debian/Ubuntu, or sudo apachectl configtest && sudo systemctl reload httpd on RHEL-family systems. .htaccess changes apply immediately. Refused requests appear in the error log as AH01630: client denied by server configuration for methods 2 and 3; for mod_rewrite, use LogLevel rewrite:trace3 as explained in the mod_rewrite guide.

To see who is hotlinking you before you block anything, count image requests by referring host in the combined access log (/var/log/apache2/access.log or /var/log/httpd/access_log):

awk -F'"' '$2 ~ /\.(gif|jpe?g|png|webp|avif|svg)[ ?]/ && $4 != "-" && $4 !~ /^https?:\/\/([a-z0-9-]+\.)*example\.com(:[0-9]+)?(\/|$)/ {split($4, u, "/"); print u[3]}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -20

nginx and Cloudflare equivalents

If nginx serves your images, its referer module does the same job. none allows a missing Referer, blocked allows values that do not start with http:// or https://, and matching is case-insensitive:

location ~* \.(gif|jpe?g|png|webp|avif|svg)$ {
    valid_referers none blocked server_names example.com *.example.com;
    if ($invalid_referer) {
        return 403;
    }
}

Cloudflare has a one-switch Hotlink Protection feature (in the dashboard under Security settings; the documentation files it under Scrape Shield) for traffic that passes through its proxy. It blocks requests whose Referer is present and not your domain, covers gif, ico, jpg, jpeg and png files, and lets anything in a folder named hotlink-ok through. Cloudflare notes that it does not affect crawling but will keep your images out of sites such as Google Images, Pinterest and Facebook. Use Configuration Rules to switch it off for selected paths. Other CDNs have similar Referer rules; check their documentation.

A browser-side extra. The Cross-Origin-Resource-Policy response header tells browsers to refuse cross-site no-cors loads, which include plain <img> tags, without looking at the Referer: Header set Cross-Origin-Resource-Policy "same-site" (mod_headers, AllowOverride FileInfo). The response is the same for everyone, so it is cache-friendly. It has no allow-list, though, so it also blocks image search previews and any partner you want to allow.

Sources