Emergency Apache Filters for Piwigo Gallery Scrapers

Emergency Event Summary

If your Piwigo gallery suddenly starts throwing huge bandwidth numbers, endless direct image hits, i.php?/galleries/requests, or action.php?download=&id=###&part=e downloads, you may not have time to learn Cloudflare pages, 500 menus, and syntax of their expressions. Or wait for DNS propagation. Or check every IP manually to find patterns. You may be dealing with so much flooding data, you can't parse it fast enough for patterns. This guide is meant as a fast Apache .htaccess bandage for an emergency. Not perfect security. Not a permanent solution either. But something you can upload in 15 minutes to stop the worst PHP and bandwidth drain or any disruption to gallery functions until you can find better answers.

Important: this scraper changed tactics repeatedly every 30 days. Do not assume one rule will catch it forever. It began with clunky file pulling behavior. Then moved into dynamic Piwigo image endpoints, and rotating IP pools with referrer spoofs. Then it slammed with high volume distributed requests that were peaking at 44/sec, and later referrer changes. Check your logs after every change of behavior. It may be ramping up or learning a new tactic. It gets more and more aggressive each month and is basically a Piwigo-centric mass attack designed hydra.

I've never seen anything like it. I highly suspect it's pulling images for LLM/AI thus the desperation and extremes. The situation in June was so severe, I compiled the ASNs, separated whose data goes to which, and filed abuse reports. "Filing abuse complaints makes it their problem. They don't want it to be their problem." I have found this sentiment to be true. The second you show up with scary logs that have 44/sec requests, they want you 'gone'.

In my June 2026 logs, the blocked scrape reached 150,270 blocked file requests, 143,109 unique IPs, a top minute of 1,086 requests, a per second of 44 requests, and hour of 18,703 requests. That is why I no longer treat this as "just annoying bot traffic" from some scraper that will stop one day and why I was willing to be pedantic and report abuse to every email possible. On shared hosting, the request rate matters even when most of the payload was denied.

How the scraper looks in Piwigo logs

The pattern is more obvious in the raw access logs than it is a bandwidth chart. Look for repeated hits on these types of URLs.

  • /galleries/.../filename.jpg ; direct original gallery image requests.
  • /_data/i/galleries/.../filename-me.jpg or filename-cu_s250x9999.jpg ; Piwigo generated display images.
  • /i.php?/galleries/.../filename-cu_s250x9999.jpg ; dynamic Piwigo image delivery.
  • /action.php?download=&id=#####&part=e ; Piwigo download endpoint for original files.
  • Suspicious HEAD requests, Range requests, and requests with no natural gallery navigation trail of a human.
cPanel display of normal Piwigo related gallery traffic from a human
Normal Piwigo human behavior demonstrated by me.

A normal visitor loads HTML pages, thumbnails "-th.jpg", scripts, CSS "nf3gjj.css" etc or JS "cd41zf.js" etc, special pngs for themes "icons_sprite-hover.png", and then maybe larger images are downloaded as they browse. The scraper pattern is different. It often skips the human path and goes straight for the image/file endpoints with incomprehensible paths, nonsense navigation, missing CSS/JS, no theme files or fonts etc.

Observed behavior phases

April/May: annoying but still recognizable

In the earlier phases, the scrape looked more like classic bandwidth nuisance traffic and is the type of thing that usually drops off after a while. It has repeated file pulls, HEAD requests, Range requests, direct image URLs, and some abuse of action.php download links. It was ugly, but still possible to connect many requests to a smaller set of source IPs with a range block, deny weird UA strings it used, and put basic Apache in place. Then ignore it.

cPanel bandwidth view showing unusual gallery traffic growth
First warning sign was not a dramatic bomb of data. It was slow cPanel bandwidth inflation from silly 0/1/data HEAD requests.

June: extreme distributed flood behavior

By June, the behavior changed and the scraper moved into an absurd rotating pool and excessive per second requests. The highest single IP only made 13 blocked requests, while the total pool was 150k+ blocked requests. The worst minute had 1,086 requests from 1,086 unique IPs. That is the point where IP by IP blocking stops being enough.

I started blocking things mid May and the result is noticeably improved. I didn't realize I was being raided in April. I don't check my gallery's data often. Not until I saw inflated bandwidth in May above the average and wondered why. The 30k that slipped through in June was because it changed behaviors at the start of the month and I didn't notice until some days in to put a stop.

MonthPhoto/file requestsBlockedTop minuteTop hour
April16,382252279/min2,269/hr
May17,7634,282320/min1,296/hr
June187,434150,2971,086/min18,708/hr

Apache rule choices

Stage 1: low risk emergency filter

This is the first layer I would try during an active incident and what I was using early on in May. It denies obvious annoying automated tool user agents and plus HEAD and Range against Piwigo image endpoints.

# Piwigo emergency scrape filter
# Place near the top of .htaccess, before hotlink rules
ErrorDocument 403 "Forbidden"
RewriteEngine On

# Obvious automated tool user agents hitting image endpoints.
RewriteCond %{HTTP_USER_AGENT} "(?:Python|aiohttp|curl|wget|httpx|requests|Go-http-client|libwww-perl|Scrapy)" [NC]
RewriteRule ^(?:galleries|_data/i/galleries)/ - [F,L]

RewriteCond %{HTTP_USER_AGENT} "(?:Python|aiohttp|curl|wget|httpx|requests|Go-http-client|libwww-perl|Scrapy)" [NC]
RewriteCond %{QUERY_STRING} ^/galleries/.*\.(?:jpe?g|png|gif|webp)(?:$|&) [NC]
RewriteRule ^i\.php$ - [F,L]

# HEAD / Range probes against static Piwigo images
RewriteCond %{REQUEST_METHOD} =HEAD [OR]
RewriteCond %{HTTP:Range} . [OR]
RewriteCond %{HTTP:Request-Range} .
RewriteRule ^(?:galleries|_data/i/galleries)/.*\.(?:jpe?g|png|gif|webp)$ - [F,L,NC]

# HEAD / Range probes against Piwigo's dynamic image endpoint
RewriteCond %{QUERY_STRING} ^/galleries/.*\.(?:jpe?g|png|gif|webp)(?:$|&) [NC]
RewriteCond %{REQUEST_METHOD} =HEAD [OR]
RewriteCond %{HTTP:Range} . [OR]
RewriteCond %{HTTP:Request-Range} .
RewriteRule ^i\.php$ - [F,L]

Stage 2: disable abused original file downloads

If your logs show repeated action.php?download=&id=###&part=e, you can temporarily block that. In a huge flood of them, you may want to stop the damage now before allowing it again. But this will prevent even humans and only is an emergency code.

# Optional emergency layer: block Piwigo original file downloads.

RewriteCond %{QUERY_STRING} "(^|&)download(?:=|&|$)" [NC]
RewriteCond %{QUERY_STRING} "(^|&)id=[0-9]+" [NC]
RewriteCond %{QUERY_STRING} "(^|&)part=e(?:&|$)" [NC]
RewriteRule ^action\.php$ - [F,L]

Stage 3: rotating prefix block

This is useful when your logs show the same network prefixes again and again but you haven't time or solved ASN correlations yet but you see an obvious repeat in IPs by eye. This answer may also be viable if the ASN being used is somehow one you don't want to block or can't.

# Rotating pool starter block.
# This is a starter example only from my logs. Replace with the IP prefixes active in your own logs.
# Format: first\.second\. | first\.second\.

RewriteCond %{REMOTE_ADDR} ^(?:1\.92\.|14\.191\.|47\.79\.|82\.157\.|101\.43\.|113\.44\.|116\.204\.|120\.53\.|121\.37\.|123\.20\.|140\.143\.|222\.252\.)
RewriteRule ^(?:_data/i/galleries|galleries)/.*\.(?:jpe?g|png|gif|webp)$ - [F,L,NC]

RewriteCond %{REMOTE_ADDR} ^(?:1\.92\.|14\.191\.|47\.79\.|82\.157\.|101\.43\.|113\.44\.|116\.204\.|120\.53\.|121\.37\.|123\.20\.|140\.143\.|222\.252\.)
RewriteCond %{QUERY_STRING} ^/galleries/.*\.(?:jpe?g|png|gif|webp)(?:$|&) [NC]
RewriteRule ^i\.php$ - [F,L]

Stage 4: cookie gate on strict mode

This is the stronger stage. A real browser receives the cookie by loading a normal Piwigo page first and direct image pulls without the cookie are then denied. This catches a lot of scrapers that request image files directly without behaving like a person browsing the gallery. This is a 24 cookie since this is a low end scraper.

# Cookie gate; receives this cookie by loading index.php or picture.php page first.
# Direct image pulls without the cookie are denied.
# Rename example.com to your's and PIWIGO_OK to something unique for your site. This part is customizable.

<IfModule mod_headers.c>
  <FilesMatch "^(index|picture)\.php$">
    Header always set Set-Cookie "PIWIGO_OK=1; Max-Age=86400; Path=/; Domain=.example.com; Secure; SameSite=Lax"
  </FilesMatch>
</IfModule>

# Allow major search image bots by UA if you care about image indexing.
# Note: UA strings can be spoofed. If you see odd IPs for these bots not proper for their services, then you have a bigger problem.

RewriteCond %{HTTP_COOKIE} !(^|;\s*)PIWIGO_OK=1(;|$)
RewriteCond %{HTTP_USER_AGENT} !(?:Googlebot|Googlebot-Image|GoogleOther-Image|Google-InspectionTool|bingbot|BingPreview|Applebot|DuckDuckBot|YandexImages|Pinterestbot) [NC]
RewriteRule ^(?:_data/i/galleries|galleries)/.*\.(?:jpe?g|png|gif|webp)$ - [F,L,NC]

RewriteCond %{HTTP_COOKIE} !(^|;\s*)PIWIGO_OK=1(;|$)
RewriteCond %{HTTP_USER_AGENT} !(?:Googlebot|Googlebot-Image|GoogleOther-Image|Google-InspectionTool|bingbot|BingPreview|Applebot|DuckDuckBot|YandexImages|Pinterestbot) [NC]
RewriteCond %{QUERY_STRING} ^/galleries/.*\.(?:jpe?g|png|gif|webp)(?:$|&) [NC]
RewriteRule ^i\.php$ - [F,L]

Stage 5: cookie gate on embed friendly mode

If your main site embeds images from a Piwigo subdomain like me, a strict mode would block those embedded images until the visitor has loaded a gallery page. Which they might not do. This variant allows your main domain as a referrer. It is more convenient, but weaker because referrers can be faked. But if you do not see that type of behavior yet and still have time to get a WAF setup, this may be a good hold over bandage.

# Cookie gate v2 - use this only if your main website embeds images from your Piwigo subdomain.
# Replace example.com with your main domain.

<IfModule mod_headers.c>
  <FilesMatch "^(index|picture)\.php$">
    Header always set Set-Cookie "PIWIGO_OK=1; Max-Age=86400; Path=/; Domain=.example.com; Secure; SameSite=Lax"
  </FilesMatch>
</IfModule>

RewriteCond %{HTTP_COOKIE} !(^|;\s*)PIWIGO_OK=1(;|$)
RewriteCond %{HTTP_USER_AGENT} !(?:Googlebot|Googlebot-Image|GoogleOther-Image|Google-InspectionTool|bingbot|BingPreview|Applebot|DuckDuckBot|YandexImages|Pinterestbot) [NC]
RewriteCond %{HTTP_REFERER} !^https?://(?:www\.)?example\.com(?:/|$) [NC]
RewriteRule ^(?:_data/i/galleries|galleries)/.*\.(?:jpe?g|png|gif|webp)$ - [F,L,NC]

RewriteCond %{HTTP_COOKIE} !(^|;\s*)PIWIGO_OK=1(;|$)
RewriteCond %{HTTP_USER_AGENT} !(?:Googlebot|Googlebot-Image|GoogleOther-Image|Google-InspectionTool|bingbot|BingPreview|Applebot|DuckDuckBot|YandexImages|Pinterestbot) [NC]
RewriteCond %{HTTP_REFERER} !^https?://(?:www\.)?example\.com(?:/|$) [NC]
RewriteCond %{QUERY_STRING} ^/galleries/.*\.(?:jpe?g|png|gif|webp)(?:$|&) [NC]
RewriteRule ^i\.php$ - [F,L]

🔥 Panic button version 🔥

This is the "my Piwigo gallery is literally on FIRE mid-50k request session" version. Edit the cookie name, domain, and IP prefix list before using it. Put it near the top of your .htaccess, before hotlink protection and before Piwigo's normal rewrite block. (Depends on the installation and your host. I don't have this. My .htaccess is my own playground.) This should take 15 minutes to do and put the giant bandage on for you to buy numerous hours to get Cloudflare, Fastly, Bunny.net Shield, or any other WAF on boarded. Once you can breathe, consider blocking UA strings below.

If you are here and don't know what an .htaccess even is, you need to go to this plugin and try that method. I can't help you on that front.

# Piwigo emergency scrape filter
# Edit example.com to your domain, PIWIGO_OK to a custom name for your site, and the REMOTE_ADDR's list with your specific IP spam blocks before use.

ErrorDocument 403 "Forbidden"
RewriteEngine On

# 1) Deny obvious automation on image endpoints
RewriteCond %{HTTP_USER_AGENT} "(?:Python|aiohttp|curl|wget|httpx|requests|Go-http-client|libwww-perl|Scrapy)" [NC]
RewriteRule ^(?:galleries|_data/i/galleries)/ - [F,L]

RewriteCond %{HTTP_USER_AGENT} "(?:Python|aiohttp|curl|wget|httpx|requests|Go-http-client|libwww-perl|Scrapy)" [NC]
RewriteCond %{QUERY_STRING} ^/galleries/.*\.(?:jpe?g|png|gif|webp)(?:$|&) [NC]
RewriteRule ^i\.php$ - [F,L]

# 2) Deny HEAD / Range image probes
RewriteCond %{REQUEST_METHOD} =HEAD [OR]
RewriteCond %{HTTP:Range} . [OR]
RewriteCond %{HTTP:Request-Range} .
RewriteRule ^(?:galleries|_data/i/galleries)/.*\.(?:jpe?g|png|gif|webp)$ - [F,L,NC]

RewriteCond %{QUERY_STRING} ^/galleries/.*\.(?:jpe?g|png|gif|webp)(?:$|&) [NC]
RewriteCond %{REQUEST_METHOD} =HEAD [OR]
RewriteCond %{HTTP:Range} . [OR]
RewriteCond %{HTTP:Request-Range} .
RewriteRule ^i\.php$ - [F,L]

# 3) Optional: disable original downloads during an active scrape
RewriteCond %{QUERY_STRING} "(^|&)download(?:=|&|$)" [NC]
RewriteCond %{QUERY_STRING} "(^|&)id=[0-9]+" [NC]
RewriteCond %{QUERY_STRING} "(^|&)part=e(?:&|$)" [NC]
RewriteRule ^action\.php$ - [F,L]

# 4) Optional: block active rotating prefixes seen in your own logs
RewriteCond %{REMOTE_ADDR} ^(?:1\.92\.|14\.191\.|47\.79\.|82\.157\.|101\.43\.|113\.44\.|116\.204\.|120\.53\.|121\.37\.|123\.20\.|140\.143\.|222\.252\.)
RewriteRule ^(?:_data/i/galleries|galleries)/.*\.(?:jpe?g|png|gif|webp)$ - [F,L,NC]

RewriteCond %{REMOTE_ADDR} ^(?:1\.92\.|14\.191\.|47\.79\.|82\.157\.|101\.43\.|113\.44\.|116\.204\.|120\.53\.|121\.37\.|123\.20\.|140\.143\.|222\.252\.)
RewriteCond %{QUERY_STRING} ^/galleries/.*\.(?:jpe?g|png|gif|webp)(?:$|&) [NC]
RewriteRule ^i\.php$ - [F,L]

# 5) Cookie gate emergency button
<IfModule mod_headers.c>
  <FilesMatch "^(index|picture)\.php$">
    Header always set Set-Cookie "PIWIGO_OK=1; Max-Age=86400; Path=/; Domain=.example.com; Secure; SameSite=Lax"
  </FilesMatch>
</IfModule>

RewriteCond %{HTTP_COOKIE} !(^|;\s*)PIWIGO_OK=1(;|$)
RewriteCond %{HTTP_USER_AGENT} !(?:Googlebot|Googlebot-Image|GoogleOther-Image|Google-InspectionTool|bingbot|BingPreview|Applebot|DuckDuckBot|YandexImages|Pinterestbot) [NC]
RewriteRule ^(?:_data/i/galleries|galleries)/.*\.(?:jpe?g|png|gif|webp)$ - [F,L,NC]

RewriteCond %{HTTP_COOKIE} !(^|;\s*)PIWIGO_OK=1(;|$)
RewriteCond %{HTTP_USER_AGENT} !(?:Googlebot|Googlebot-Image|GoogleOther-Image|Google-InspectionTool|bingbot|BingPreview|Applebot|DuckDuckBot|YandexImages|Pinterestbot) [NC]
RewriteCond %{QUERY_STRING} ^/galleries/.*\.(?:jpe?g|png|gif|webp)(?:$|&) [NC]
RewriteRule ^i\.php$ - [F,L]

IP / ASN starter list

Apache logs normally show IPs, not ASNs. In a hurry, I recommend checking a few hundred blocked IPs from the flood and looking for repeated prefixes. If your active scrape has a strong overlap with the list below, you can use that shortcut. If it does not overlap, do not blindly use this.

PrefixNote
1.92.Repeated in Apr-June excessively - main source
14.191.Vietnam VNPT high volume
47.79.Alibaba Cloud high volume
82.157.Tencent Cloud high volume
101.43.Tencent Cloud high volume
113.44.Huawei Cloud high volume
116.204.Huawei Cloud high volume
120.53.Tencent Cloud high volume
121.37.Huawei Cloud high volume
123.20.Vietnam VNPT high volume
140.143.Tencent Cloud high volume
222.252.Vietnam VNPT high volume
AS45090Shenzhen Tencent Computer Systems Company Limited
AS132203Tencent Building, Kejizhongyi Avenue
AS45102Alibaba (US) Technology Co., Ltd.
AS55990Huawei Cloud Service data center
AS45899VNPT Corp

User agent starter list

If you can't look at IPs for patterns quickly or you feel there's too many, too fast - you can start blocking garbage UA strings immediately. Here's a starter block of the dumbest ones that no human would use. You need to update the browser blocks to suit what is happening to your site specifically and what the current versions are since things like Chrome, Opera, and Firefox receive updates regularly.

# "[S=8]" needs to be customized to how many you add or delete. The count matters. You can customize the bots.
# allow declared good bots before old browser blocks
RewriteCond %{HTTP_USER_AGENT} (Applebot|Googlebot|Google-InspectionTool|bingbot|DuckDuckBot|facebookexternalhit|Meta-ExternalAgent|OAI-SearchBot|ClaudeBot|Pinterestbot) [NC]
RewriteRule ^ - [S=8]

# Block old Firefox below 144
RewriteCond %{HTTP_USER_AGENT} Firefox/(?:[1-9][0-9]?|1[0-3][0-9]|14[0-3])\. [NC]
RewriteRule ^ - [F]

# Block old plain Chrome below 144
RewriteCond %{HTTP_USER_AGENT} "(^|[ (;])Chrome/(?:[1-9]|[1-9][0-9]|1[01][0-9]|12[0-9]|13[0-9]|14[0-3])\." [NC]
RewriteRule ^ - [F,L]

# Block old CriOS below 144
RewriteCond %{HTTP_USER_AGENT} "(^|[ (;])CriOS/(?:[1-9][0-9]?|1[01][0-9]|12[0-9]|13[0-9]|14[0-3])\." [NC]
RewriteRule ^ - [F,L]

# Block very old Mac Chrome
RewriteCond %{HTTP_USER_AGENT} "Macintosh; Intel Mac OS X 10_15_7" [NC]
RewriteCond %{HTTP_USER_AGENT} Chrome/(10[0-9]|11[0-9]|12[0-9]|13[0-9])\. [NC]
RewriteRule ^ - [F]

# Block old Chromium below 142
RewriteCond %{HTTP_USER_AGENT} "(^|[ (;])Chromium/(?:[1-9][0-9]?|1[01][0-9]|12[0-9]|13[0-9]|14[0-1])\." [NC]
RewriteRule ^ - [F,L]

# Block old Safari
RewriteCond %{HTTP_USER_AGENT} "Version/(?:[1-9]|1[0-5])\.[0-9].*Safari/" [NC]
RewriteRule ^ - [F,L]

# Block old Opera / OPR below 126
RewriteCond %{HTTP_USER_AGENT} "(^|[ (;])OPR/(?:[1-9]|[1-9][0-9]|10[0-9]|11[0-9]|12[0-5])\." [NC]
RewriteRule ^ - [F,L]

# Block old/alternate Opera below 127
RewriteCond %{HTTP_USER_AGENT} "(^|[ (;])Opera/(?:[1-9]|[1-9][0-9]|10[0-9]|11[0-9]|12[0-6])\." [NC]
RewriteRule ^ - [F,L]

Checking your own logs

If your host gives you raw access logs, look at them for stuff like:

  • A sudden increase in strange image requests.
  • Many requests to /_data/i/galleries/, /galleries/, /i.php?/galleries/, or /action.php?download.
  • Large numbers of unique IPs making only 1-3 requests each.
  • Bursts where hundreds of requests in the same minute.
  • Odd browser UAs that rotate versions and seem like nonsense.
  • Referrers that look plausible but do not match a browsing path of any person.

Limits and warnings

  • This is a bandage. Not a defense force. It helps stop the bleeding while you decide whether to add Cloudflare or another WAF, learn how to do that and read docs, wait for DNS to propagate, find rate limiting features, or buy time to contact an admin/host etc. If your site is flooding and normal humans can't use the gallery, this would reduce the load until better mitigation.
  • Do not block every image request. Piwigo needs image files for real visitors.
  • Back up your .htaccess first. One typo can white screen or 500 error your gallery. I've caused my own 500 redirect loop on my domain...
  • Test after upload. Open a gallery category, an image page, a thumbnail, a large image, and any main site embeds after to make sure you, a good human, have everything functioning. Because if you do, most others should be good to go too.
Small default 403 Forbidden response caused by Apache denying a request
A tiny 403 9 byte response is ugly in the logs, but much bandwidth/rate cheaper than letting Piwigo send files in the meanwhile.

My own conclusion after watching this evolve into a Piwigo hydra to slay: put the low risk layers in place before it becomes an emergency. If you own any Piwigo gallery, you should put some of this in place in advance and be ready for it. In April it looked like a dumb nuisance. By June, the scraper had become a destruction level distributed flood that will have absolutely tanked Piwigo's PHP endpoints with more than 18,000 blocked attempts in one hour. Imagine what this would have been if I hadn't done that preemptively and kept up with it since May.