mattintosh note

どこかのエンジニアモドキの備忘録

2024-06-05: 現在ホビー関連の記事を 新しいブログ に移行しています(一部の国、ISP からは閲覧できません)

Apacheでブラウザのバージョンに応じてアクセス制限をかけたい

アクセスログを見ていると稀にユーザーエージェントに Chrome/98Firefox/48 というようなものを見かけることがあります。ご丁寧にユーザーエージェントを改ざんしているけどどう考えてもおかしいものです(Firefox 48 なんて 2016 年リリース)。

Cloudflare WAF ではこれらのブラウザのバージョンを細かく指定することができません(Cloudflare に限らず難しいとは思いますが)。出来ることとしては部分一致で指定する方法です。以下のように指定することで Google Chrome の 90 番台のバージョンを指定することができます。2024 年 8 月現在の Google Chrome の最新バージョンは 127.0.0.0 で、バージョン 900 番台に到達するのは恐らく数十年後でしょうし、それまで User-Agent が使われているかどうかもわかりませんので問題は無いでしょう。

lower(http.user_agent) contains "chrome/9"

同じように chrome/8chrome/7 と追加していたんですが最近 Cloudflare WAF の容量問題(4 KB まで)でルールを少し整理する必要が出てきたのでいくつかのルールを Web サーバーの方に移すことにしました。

Apache にはいくつか数値比較が出来る手段が用意されていますが、今回は mod_rewrite を使うことにします(他には <If>Require expr を使う方法があります)。うちのブログは万人に見てもらいたいわけではないので ChromeFirefox の 100 未満を使っているものはアクセス拒否してしまいます。User-Agent が空のものは Cloudflare で len(http.user_agent) eq 0 で判定してブロック可能ですがこちらにも BrowserMatch ^$(または BrowserMatchNoCase ^$)として条件に入れてあります。

# 初期値設定
BrowserMatch . APPLEWEBKIT_VERSION=999
BrowserMatch .      CHROME_VERSION=999
BrowserMatch .        EDGE_VERSION=999
BrowserMatch .     FIREFOX_VERSION=999
BrowserMatch .     MOZILLA_VERSION=999
BrowserMatch .      SAFARI_VERSION=999
BrowserMatch .        DENY_VERSION=999
# バージョン取得
BrowserMatchNoCase   AppleWebKit/(\d+) APPLEWEBKIT_VERSION=$1
BrowserMatchNoCase        Chrome/(\d+)      CHROME_VERSION=$1
BrowserMatchNoCase         CriOS/(\d+)      CHROME_VERSION=$1
BrowserMatchNoCase Edg(A|e|iOS)?/(\d+)        EDGE_VERSION=$1
BrowserMatchNoCase       Firefox/(\d+)     FIREFOX_VERSION=$1
BrowserMatchNoCase       Mozilla/(\d+)     MOZILLA_VERSION=$1
BrowserMatchNoCase        Safari/(\d+)      SAFARI_VERSION=$1
BrowserMatchNoCase ^$                        !DENY_VERSION
BrowserMatchNoCase AhrefsBot                 !DENY_VERSION
BrowserMatchNoCase GPTBot                    !DENY_VERSION
BrowserMatchNoCase MJ12bot                   !DENY_VERSION
BrowserMatchNoCase MSIE                      !DENY_VERSION
BrowserMatchNoCase SemrushBot                !DENY_VERSION
BrowserMatchNoCase Trident                   !DENY_VERSION
# 正規のボットを除外(要 Cloudflare カスタムヘッダー)
RewriteCond %{HTTP_X_CLIENT_BOT} !=true
# バージョン指定
RewriteCond %{ENV:APPLEWEBKIT_VERSION} -lt537 [OR]
RewriteCond %{ENV:CHROME_VERSION}      -lt100 [OR]
RewriteCond %{ENV:EDGE_VERSION}        -lt100 [OR]
RewriteCond %{ENV:FIREFOX_VERSION}     -lt100 [OR]
RewriteCond %{ENV:MOZILLA_VERSION}     -lt5   [OR]
RewriteCond %{ENV:SAFARI_VERSION}      -lt537 [OR]
RewriteCond %{ENV:DENY_VERSION}        -lt999
# アクセス拒否
RewriteRule ^ - [F]

まず、BrowserMatch(または SetEnvIf User-Agent)で各ブラウザの初期値を設定します。 これは RewriteCond の数値比較では定義されていない変数の値が 0 として計算されるためです。なお、SetEnv による変数の定義は後から上書き出来ないようです。

BrowserMatch . APPLEWEBKIT_VERSION=999

次に User-Agent の値からメージャーバージョンを取得します。例えば User-Agent に AppleWebKit/537.36 が含まれる場合、APPLEWEBKIT_VERSION の値は 999 から 537 に変更されます。出来ればマイナーバージョンまで比較したいところですが Apache では整数でしか比較できないようです。

BrowserMatchNoCase AppleWebKit/(\d+) APPLEWEBKIT_VERSION=$1

最後に RewriteCond の数値比較で指定した値よりも古いバージョンを拒否するようにします。

RewriteCond %{ENV:APPLEWEBKIT_VERSION} -lt537
RewriteRule ^ - [F]

ここで注意しなければならないのが善良なクローラーやボットが未だに古いバージョンの User-Agent を指定している場合です。Cloudflare で cf.client.bot または cf.bot_management.verified_bot を任意のヘッダー(例:X-CLIENT-BOT)としてヘッダーに追加している場合はこの値が true として送られてくるのでこれを使えばいいのですが、Cloudflare を使っていない場合はボットの User-Agent で指定する必要があるでしょう。User-Agent のエスケープに関しては PHPpreg_quote() 関数などを使うのが楽です。

※この User-Agent に偽装された場合でも適切に正規のボットを識別できるよう Cloudflare の cf.client.bot または cf.bot_management.verified_bot の使用を推奨します。

RewriteCond %{HTTP_USER_AGENT} "!^Mozilla/5\.0 AppleWebKit/537\.36 \(KHTML, like Gecko; compatible; Googlebot/2\.1; \+http\://www\.google\.com/bot\.html\) Chrome/\d+\.\d+\.\d+\.\d+ Safari/537\.36$"
RewriteCond %{HTTP_USER_AGENT} "!^Mozilla/5\.0 \(Linux; Android 6\.0\.1; Nexus 5X Build/MMB29P\) AppleWebKit/537\.36 \(KHTML, like Gecko\) Chrome/\d+\.\d+\.\d+\.\d+ Mobile Safari/537\.36 \(compatible; Googlebot/2\.1; \+http\://www\.google\.com/bot\.html\)$"
RewriteCond %{HTTP_USER_AGENT} "!^Mozilla/5\.0 \(compatible; Googlebot/2\.1; \+http\://www\.google\.com/bot\.html\)$"
RewriteCond %{ENV:APPLEWEBKIT_VERSION} -lt537
RewriteRule ^ - [F]

バージョンに関係なくブロックしたい User-Agent は変数を削除することで値が 0 となるため、下記の数値比較は 0 < 999 となりアクセスは拒否されます。なお、これに関してはもっと楽な記述で拒否できます。

BrowserMatch . DENY_VERSION=999
BrowserMatchNoCase MJ12bot !DENY_VERSION
RewriteCond %{ENV:DENY_VERSION} -lt999
RewriteRule ^ - [F]

# 単純な拒否に関してはこっちの方が簡潔
RewriteCond %{HTTP_USER_AGENT} MJ12bot [NC]
RewriteRule ^ - [F]

比較演算子<<= もありますがこれらは文字列での比較になるため誤った結果になる可能性があります。例えば下記は数値的に見れば同じ値ですが右辺の方が大きい結果になります。

RewriteCond 100.0 <100.00

Edge に関しては User-Agent が酷い仕様で Edge かと思いきや実は Edg。他にも EdgeEdgAEdgiOS があるようです。Windows 10 Mobile とか Xbox の場合は Chrome のバージョンと一致しないようなんですが面倒くさいからいいや…。

https://www.whatismybrowser.com/guides/the-latest-user-agent/edge

迷惑なアクセスは本当に滅んで欲しいですね。