M
MeshWorld.
HowTo Security Web Performance CDN 14 min read

How to Prevent Image Hotlinking in 2026

Vishnu
By Vishnu
| Updated: Mar 27, 2026

Image hotlinking is when another website embeds your images directly using your server’s URL. Their page loads your image. Your server pays the bandwidth bill. You get nothing except a spike in egress costs and a slower site for your own visitors. It’s not hacking — it’s just bad manners baked into how HTTP works. This guide covers how to block it at every layer: Apache, Nginx, Cloudflare, AWS CloudFront, Vercel, and Netlify. Pick the one that matches your stack and you’ll be protected in under ten minutes.

:::note[TL;DR]

  • Apache: Add mod_rewrite rules to .htaccess checking the HTTP_REFERER value
  • Nginx: Use valid_referers to return 403 for unauthorized domains
  • Cloudflare: One toggle — Scrape Shield > Hotlink Protection (free plan included)
  • AWS CloudFront: Deploy a CloudFront Function that checks the Referer header
  • Vercel: Middleware checking Referer (headers config alone won’t cut it for images)
  • Netlify: _redirects rule with a Referer condition, or netlify.toml headers :::

Prerequisites

  • Apache/Nginx: SSH access to your server and permission to edit .htaccess or the server config
  • Cloudflare: A domain proxied through Cloudflare (orange cloud enabled)
  • AWS CloudFront: Access to the AWS Console and an existing CloudFront distribution
  • Vercel/Netlify: Access to your project dashboard or the ability to deploy config file changes

What is image hotlinking and why does it cost you money?

When a browser loads a webpage, it fetches every resource — HTML, CSS, JS, images — from wherever those URLs point. If someone writes <img src="https://yoursite.com/uploads/photo.jpg"> on their site, every visitor to their page downloads that image directly from your server. You’re serving the bandwidth. They’re showing the content.

It adds up fast. A single image embedded in a popular forum thread can generate thousands of requests per hour. On a VPS or S3 bucket with pay-as-you-go egress, that’s real money. On a shared host, it can get your account suspended for exceeding bandwidth limits.

The Scenario: You run a photography blog. A popular Reddit thread finds one of your photos and hotlinks it instead of uploading a copy. The thread hits the front page. By morning you’ve burned through 40 GB of bandwidth — most of it not your own visitors — and your hosting provider sends you a bill you weren’t expecting. You didn’t even know it was happening until the invoice arrived.

The fix is to check the Referer HTTP header on image requests. If the request comes from a domain you don’t recognize, you block it or serve a placeholder instead.


How do I block hotlinking in Apache (.htaccess)?

Apache’s mod_rewrite module lets you inspect the HTTP_REFERER header on every request. The rules below allow requests with no referer (direct access, bookmarks, curl without -e), allow your own domain, and block everything else.

Add these rules to your .htaccess file in the web root, or inside a <Directory> block in your virtual host config:

RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?yourdomain\.com/ [NC]
RewriteRule \.(jpg|jpeg|png|gif|webp|svg|avif)$ - [F,L]

The first RewriteCond passes empty referers (direct access is fine). The second passes requests from your own domain. Anything else hits the [F] flag — a 403 Forbidden. The [NC] flag makes it case-insensitive.

:::tip To serve a placeholder image instead of a 403, replace - [F,L] with the path to your placeholder: RewriteRule \.(jpg|jpeg|png|gif|webp|svg|avif)$ /images/no-hotlinking.png [R,L]. Just make sure the placeholder isn’t itself an image type that triggers the rule — it’ll loop. :::

:::warning If your site sits behind a CDN or reverse proxy, the HTTP_REFERER Apache sees may be the CDN’s IP, not the real referring domain. In that case, block hotlinking at the CDN layer (Cloudflare, CloudFront) instead — Apache-level rules won’t help you there. :::

The Scenario: Your WordPress site runs on a shared host and you can’t touch the server config directly — only .htaccess. The three lines above take thirty seconds to add via FTP or the file manager in cPanel. That’s the whole fix.


How do I block hotlinking in Nginx?

Nginx handles this with the valid_referers directive inside a location block. The approach is the same: allow blank referers, allow your own domain, block the rest.

Open your site’s Nginx config (usually in /etc/nginx/sites-available/yoursite) and add a location block for image files:

location ~* \.(jpg|jpeg|png|gif|webp|svg|avif)$ {
    valid_referers none blocked server_names ~\.yourdomain\.com;
    if ($invalid_referer) {
        return 403;
    }
}

none allows requests with no Referer header. blocked allows requests where the header exists but has been stripped (some firewalls and proxies do this). server_names allows requests from the server’s own domain. The ~\.yourdomain\.com regex lets you match subdomains too.

After editing, test the config and reload:

sudo nginx -t && sudo systemctl reload nginx

Always run nginx -t before reloading. A typo in the config file will take your site down when you reload.

:::tip Want to serve a branded “no hotlinking” image instead of a raw 403? Replace return 403 with rewrite ^ /images/no-hotlinking.png break;. The visitor sees an image, but it’s yours — and it can say whatever you want. :::

The Scenario: You’re managing a VPS at midnight because production is acting weird, and you notice in your logs that a scraper site has been embedding 200 of your product photos for the past week. You add the location block, reload Nginx in 90 seconds, and watch the request volume drop immediately in the access log.


How do I block hotlinking on Cloudflare?

Cloudflare has a built-in toggle. No config file editing, no server access needed. It’s in the dashboard under Scrape Shield.

  1. Log into the Cloudflare dashboard
  2. Select your domain
  3. Go to Scrape Shield (under the Security section in the left sidebar)
  4. Find Hotlink Protection and flip it to On

That’s it. Cloudflare will block image requests where the Referer header points to a domain that isn’t yours. It allows empty referers (direct access) by default.

:::warning Cloudflare’s basic Hotlink Protection doesn’t let you whitelist specific external domains — it’s all-or-nothing for third parties. If you need to allow specific partners or embed providers (like a portfolio site that legitimately links to your images), you’ll need a Cloudflare Worker for fine-grained control. :::

For a custom allowlist with Cloudflare Workers, deploy this:

export default {
  async fetch(request) {
    const referer = request.headers.get("Referer") || "";
    const url = new URL(request.url);
    const isImage = /\.(jpg|jpeg|png|gif|webp|svg|avif)$/i.test(url.pathname);

    const allowedDomains = ["yourdomain.com", "trustedpartner.com"];
    const isAllowed =
      referer === "" ||
      allowedDomains.some((domain) => referer.includes(domain));

    if (isImage && !isAllowed) {
      return new Response("Hotlinking not permitted.", { status: 403 });
    }

    return fetch(request);
  },
};

Deploy this Worker and add a route matching yourdomain.com/uploads/* (or wherever your images live).

The Scenario: You run a design inspiration site with hundreds of high-res images. You turned on Cloudflare’s hotlink protection and forgot about it — until a client emails saying the image gallery they embedded from your site for a demo is showing 403s. The Worker approach lets you whitelist their domain in one line without turning off protection for everyone else.


How do I block hotlinking on AWS CloudFront?

CloudFront doesn’t have a hotlink toggle. You need a CloudFront Function — a lightweight JavaScript function that runs at the edge on every request.

In the AWS Console, go to CloudFront > Functions and create a new function with this code:

function handler(event) {
  var request = event.request;
  var headers = request.headers;
  var referer = headers["referer"] ? headers["referer"].value : "";

  var allowedDomains = ["yourdomain.com", "www.yourdomain.com"];

  var isImageRequest = /\.(jpg|jpeg|png|gif|webp|svg|avif)$/i.test(
    request.uri
  );

  if (isImageRequest && referer !== "") {
    var allowed = allowedDomains.some(function (domain) {
      return referer.indexOf(domain) !== -1;
    });

    if (!allowed) {
      return {
        statusCode: 403,
        statusDescription: "Forbidden",
        body: "Hotlinking not permitted.",
      };
    }
  }

  return request;
}

After saving, publish the function and associate it with your CloudFront distribution’s Viewer Request event for the behavior that serves your images.

:::warning CloudFront Functions run on the Viewer Request event, which means before the cache is checked. This is what you want for hotlink protection — but be aware it adds a small latency cost to every image request. For most sites it’s negligible. For very high-traffic distributions, benchmark it. :::

The Scenario: Your S3 bucket is behind CloudFront and you woke up to a $140 egress bill because a forum post hotlinked your product screenshots. There’s no .htaccess to edit here — everything’s in the AWS Console. The CloudFront Function above gets deployed in about five minutes and runs at every edge location automatically.


How do I block hotlinking on Vercel?

Vercel’s vercel.json headers config lets you set response headers, but it can’t conditionally block requests based on the incoming Referer — headers config is static. For real hotlink protection on Vercel, you need Vercel Middleware.

Create a middleware.ts (or middleware.js) file at the root of your project:

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const referer = request.headers.get("referer") ?? "";
  const url = request.nextUrl.pathname;

  const isImage = /\.(jpg|jpeg|png|gif|webp|svg|avif)$/i.test(url);
  const allowedDomains = ["yourdomain.com", "www.yourdomain.com"];

  if (isImage && referer !== "") {
    const allowed = allowedDomains.some((domain) =>
      referer.includes(domain)
    );
    if (!allowed) {
      return new NextResponse("Hotlinking not permitted.", { status: 403 });
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/images/:path*", "/uploads/:path*", "/public/:path*"],
};

Update the matcher array to cover the paths where your images live. Update allowedDomains with your real domain.

:::warning This middleware runs for every matched request, including your own page loads. Test it locally with next dev before deploying — if your matcher is too broad, you’ll accidentally block your own image loading. Scope it tightly to the directories that actually contain static images. :::

The Scenario: You’re on Vercel’s free tier and your /public/og-images/ folder is getting hammered because someone scraped your OG image URLs and embedded them in a bulk email campaign. The middleware above — scoped to /public/og-images/* — blocks all of it without touching anything else.


How do I block hotlinking on Netlify?

Netlify’s redirect engine supports condition matching on request headers, including Referer. The cleanest approach is a _redirects rule. Add this to your _redirects file (in your public/ or static/ folder, wherever Netlify looks for it):

/uploads/*  /images/no-hotlinking.png  200!  Referer=!*yourdomain.com*

This redirects any request to /uploads/* where the Referer doesn’t contain your domain to your placeholder image. The 200! serves the placeholder at the original URL (no browser redirect visible to the user).

Alternatively, using netlify.toml to add a security header that signals intent (less enforcement, more signal to compliant clients):

[[headers]]
  for = "/uploads/*"
  [headers.values]
    X-Permitted-Cross-Domain-Policies = "none"
    X-Content-Type-Options = "nosniff"

:::warning Netlify’s _redirects Referer condition matching has some edge case behavior — it matches on substring, so yourdomain.com will also match notyourdomain.com. Use a more specific pattern like *.yourdomain.com* and test with curl -e "https://attacker.com" https://yoursite.netlify.app/uploads/photo.jpg to verify behavior before shipping. :::

The Scenario: You deployed a Netlify site for a client and forgot to add hotlink protection. Three months later they forward you a “why is our bandwidth usage so high?” email. You push a two-line change to _redirects, redeploy, and it’s done. No server access, no platform tickets.


What about legitimate image sharing — social media and Google Images?

Blocking all external referers will break social media previews, Google Image search results, and any legitimate embeds. You almost never want that.

The standard pattern: allow empty referers, allow your domain, allow specific known-good domains. Requests with no Referer header (direct URL visits, bookmarks, RSS readers, many API clients) should always be allowed — they’re not hotlinking.

For Apache, the whitelist looks like this:

RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?yourdomain\.com/ [NC]
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?google\.com/ [NC]
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?pinterest\.com/ [NC]
RewriteCond %{HTTP_REFERER} !^https?://t\.co/ [NC]
RewriteRule \.(jpg|jpeg|png|gif|webp|svg|avif)$ - [F,L]

Add a RewriteCond line for each domain you want to allow. The [NC] flag handles mixed-case referers.

For Nginx, extend the valid_referers line:

valid_referers none blocked server_names
    ~\.yourdomain\.com
    ~\.google\.com
    ~\.pinterest\.com
    ~t\.co;

The key insight: an empty Referer isn’t suspicious — it’s normal. Direct traffic, CLI tools, email clients, and privacy-focused browsers all strip or omit the Referer header. Only block non-empty referers from domains you don’t recognize.


Use curl with the -e flag to spoof a Referer header. If your protection is working, requests from unknown domains should return 403.

Test that an external referer gets blocked:

curl -I -e "https://attacker.com" https://yoursite.com/uploads/photo.jpg

You should see HTTP/2 403 (or HTTP/1.1 403 Forbidden).

Test that your own domain still works:

curl -I -e "https://yoursite.com/page/" https://yoursite.com/uploads/photo.jpg

You should see HTTP/2 200.

Test that direct access (no referer) still works:

curl -I https://yoursite.com/uploads/photo.jpg

Still 200. If this returns 403, your rules are too aggressive — you’re blocking empty referers, which will break legitimate access.

:::tip Open your browser’s DevTools (Network tab), reload a page with your images, and check the Referer header on the image requests. That’s exactly what your rules will see in production. If the header isn’t what you expected, your rules need adjusting. :::

The Scenario: You added the Apache rules, deployed, and now your own homepage isn’t loading images. You run the three curl commands above and discover the Referer your browser sends for images loaded on yourdomain.com includes https://www.yourdomain.com/ — but your rule was only matching yourdomain.com without the www. One extra condition line, redeploy, fixed.


Summary

  • Apache: Add mod_rewrite conditions to .htaccess — allow empty referers and your domain, block the rest with [F,L]
  • Nginx: Use valid_referers none blocked server_names inside a location block for image extensions
  • Cloudflare: Scrape Shield > Hotlink Protection toggle for simple cases; a Worker for domain whitelisting
  • AWS CloudFront: Deploy a CloudFront Function on the Viewer Request event — no native toggle exists
  • Vercel: Use Middleware (middleware.ts) — vercel.json headers alone can’t conditionally block by Referer
  • Netlify: _redirects with a Referer condition, tested carefully for substring matching edge cases
  • Always allow empty referers — direct access, bookmarks, and privacy browsers don’t send a Referer header
  • Test with curl -e before calling it done

FAQ

Does hotlink protection break Google Images? It depends on your rules. Google Images crawlers and users clicking through from Google Image Search send a Referer of https://www.google.com/. If you block all non-empty referers, you’ll block Google Images traffic. Add google.com to your allowlist explicitly if you want Google Images to keep working.

Will this affect Twitter/X, Facebook, or LinkedIn link previews? Social crawlers that generate link previews (Twitter Card bot, Facebook scraper, LinkedIn bot) often send no Referer header or send their own domain. Empty referers are allowed by default in all the configs above, so preview generation usually still works. Test by pasting your URL into each platform’s card validator.

Can I redirect hotlinked images to a branded placeholder instead of returning 403? Yes — this is actually better UX. In Apache, replace [F,L] with [R,L] and point to a placeholder. In Nginx, use rewrite ^ /images/no-hotlinking.png break;. The placeholder can say “Image not available — visit yoursite.com” and link back to you.

Does Cloudflare’s free plan include hotlink protection? Yes. The Scrape Shield > Hotlink Protection toggle is available on all Cloudflare plans including free. The Workers approach for custom allowlists requires the Workers free tier (100,000 requests/day free, then metered).

Does this work for videos, PDFs, and other file types? Yes — just extend the file extension list in your rules. For Apache, change the RewriteRule pattern from \.(jpg|jpeg|png|gif|webp|svg|avif)$ to include mp4|pdf|zip (or whatever you want to protect). The same Referer-checking logic applies to any file type.