Keeping images from adorning other sites: stop hotlinking in Apache
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).
Before you start
- Prefer the server configuration. Rules in the virtual host are read once at startup;
.htaccessfiles are read on every request (why .htaccess is slower). Use.htaccesson shared hosting. - Enable the modules. Debian/Ubuntu:
sudo a2enmod rewrite(mod_setenvif is on by default; addheadersif you use the stand-in image). RHEL, Alma, Rocky and Fedora load mod_rewrite, mod_setenvif and mod_headers by default in/etc/httpd/conf.modules.d/00-base.conf. - Know what .htaccess needs. mod_rewrite needs
AllowOverride FileInfoandOptions FollowSymLinks(orSymLinksIfOwnerMatch). The<If>method needsAuthConfig. The SetEnvIf method needsFileInfo AuthConfig. Without them you get a 500 error. - Replace example.com with your domain in every rule. In regular expressions, dots are written
\..
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:
| Referer | Result |
|---|---|
https://example.com/, https://www.example.com/page | Allowed |
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 needs | Stand-in image | Notes |
|---|---|---|---|
| mod_rewrite | FileInfo + FollowSymLinks | Yes | Most flexible; add exceptions with more RewriteCond lines |
<If> / Require expr | AuthConfig | No | Readable; adds Vary: Referer to served images |
SetEnvIf + Require env | FileInfo + AuthConfig | No | Simplest; 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:
- Match origins only. Do not write rules that need a path in the Referer of a cross-site request.
- A hotlinker can add
referrerpolicy="no-referrer"to the<img>tag and get through the empty-Referer exception. That is the price of not breaking legitimate traffic.
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
- Image search. Search engines may load the full-size image from your server while the visitor is on their results page. Blocking that can remove your images from Google Images, Pinterest and similar services. To keep them, add one condition per engine before the
RewriteRule, for exampleRewriteCond %{HTTP_REFERER} !^https?://([a-z0-9-]+\.)*google\.(com|[a-z]{2}|co\.[a-z]{2}|com\.[a-z]{2})(/|$) [NC]andRewriteCond %{HTTP_REFERER} !^https?://([a-z0-9-]+\.)*bing\.com(/|$) [NC]. - Social previews. Preview crawlers normally send no Referer and pass the empty-Referer rule. Images shown inside the social site's own pages may carry its origin; allow that domain the same way if you want those embeds.
- A press or "free to use" folder. Add
RewriteCond %{REQUEST_URI} !^/press/to exempt it. - Your own CDN or image service. A CDN or image-resizing service fetches originals from your server without a browser Referer, so it passes. But once the CDN caches an image, later requests never reach Apache, and your rule no longer runs for them. Configure hotlink protection on the CDN itself (see below).
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.
What hotlink protection cannot do
- The Referer is easy to fake. Any script can send
Referer: https://www.example.com/. The rule stops ordinary browsers embedding your images, not determined downloaders. - It does not stop copying. Anyone can save the image and upload it elsewhere. For that, use visible watermarks, smaller published sizes, and copyright takedown requests to the copying site's host.
- It is not access control. For private files use password protection or signed, expiring links generated by your application or CDN.
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
- When not to use mod_rewrite (includes Forbidding Image Hotlinking), Apache HTTP Server 2.4 documentation
- mod_rewrite, Apache HTTP Server 2.4 documentation
- RewriteRule flags, Apache HTTP Server 2.4 documentation
- Expressions in Apache HTTP Server, Apache HTTP Server 2.4 documentation
- mod_setenvif, Apache HTTP Server 2.4 documentation
- mod_authz_core, Apache HTTP Server 2.4 documentation
- Core features (If, FilesMatch, AllowOverride), Apache HTTP Server 2.4 documentation
- mod_headers, Apache HTTP Server 2.4 documentation
- RFC 9110 HTTP Semantics, section 10.1.3 Referer, IETF
- Referer header, MDN Web Docs
- Referrer-Policy header, MDN Web Docs
- Cross-Origin-Resource-Policy header, MDN Web Docs
- Module ngx_http_referer_module, nginx documentation
- Hotlink Protection, Cloudflare documentation
- 00-base.conf (modules loaded by default), CentOS Stream httpd package