AWS WAF で Scrapling のボットアクセスを検出・制限できるか検証してみた

AWS WAF で Scrapling のボットアクセスを検出・制限できるか検証してみた

アンチボット回避機能を備えた Python スクレイピングライブラリ「Scrapling」に対して、AWS WAF の多層防御(IP レートルール・JA4 フィンガープリントレートルール・Bot Control Targeted)で検出・制限できるかを実際に検証しました。
2026.05.03

Python の Web スクレイピングライブラリ「Scrapling」をご存知でしょうか。アダプティブ要素追跡・高速パーシング・複数フェッチャーの使い分けを1つのフレームワークで実現した、高機能なスクレイピングライブラリです。

一方で、TLS フィンガープリント偽装・Cloudflare Turnstile 自動バイパスといったアンチボット回避機能も備えており、不適切に利用された場合、従来の WAF では対応が難しいという課題があります。

そこで本記事では、AWS WAF の多層防御で Scrapling のアンチボット回避機能にどこまで対応できるかを実際に検証しました。結論から言うと、Bot Control Targeted の TGT_VolumetricIpTokenAbsent を含む多層防御が有効に機能することが確認できました。具体的な設定と実運用での留意点を紹介します。


Scrapling とは

ウェブサイトの構造変更を自動検知して要素を再配置するアダプティブ Web スクレイピングフレームワークです。

主要フェッチャー

クラス 概要 内部実装
Fetcher 高速 HTTP リクエスト。TLS/HTTP2 フィンガープリントをブラウザレベルで模倣 curl_cffi(curl-impersonate)
StealthyFetcher 改良版 Firefox + フィンガープリントスプーフィング。Cloudflare Turnstile を自動バイパス Camoufox
DynamicFetcher 完全ブラウザ自動化。JS 動的ページ対応 Playwright Chromium
# Chrome の TLS フィンガープリントを模倣してリクエスト
FetcherSession(impersonate='chrome')

# Camoufox で Cloudflare Turnstile をバイパス
StealthyFetcher.fetch('https://example.com', headless=True, solve_cloudflare=True)

多層防御の設計

Scrapling の各フェッチャーが持つ偽装能力に対して、AWS WAF の3層構造で対抗します。

  • IP レートルール — 単純な大量アクセスを止める
  • JA4 フィンガープリントレートルール — ツール固定の攻撃を止める
  • Bot Control Targeted — JA4 偽装まで対応した高度な攻撃を止める

なぜ JA3 ではなく JA4 なのか

impersonate='chrome' は Chrome 110 以降の ClientHello permutation(TLS 拡張の順序をランダム化する仕様)を忠実に再現します。JA3 は拡張の順序を含むハッシュのため、リクエストごとに異なる値になります。これは Scrapling のバグではなく、実際の Chrome の挙動を正確に模倣した結果です。

フィンガープリント Chrome 110+ の影響 レートルールへの有効性
JA3 拡張の順序を含むため変動する Chrome 模倣ツールには回避される
JA4 拡張をソートして正規化するため固定 ✅ 安定して機能する

WAF ログで実際に確認すると、impersonate='chrome' を使った連続アクセスで JA3 は毎回異なる値になりますが、JA4 は全件同一値になります。

# impersonate='chrome' で連続アクセスした際の WAF ログ(抜粋)
JA3=ba507dfd...  JA4=t13d1516h2_8daaf6152...
JA3=1355ef97...  JA4=t13d1516h2_8daaf6152...
JA3=8897b176...  JA4=t13d1516h2_8daaf6152...
JA3=c567ed94...  JA4=t13d1516h2_8daaf6152...

JA3 は毎回異なりますが、JA4 は全件 t13d1516h2_8daaf6152... で固定されています。

利用者レベル別の検出マトリクス

利用者のスキルレベル(偽装の精度)に応じて各レイヤーがどのように機能するかを整理します。

利用者レベル ツール例 事前観測(UA/JA4 分析)での検出 WAF での検出
Level 1 素の requests / httpx UA で即検出 SignalNonBrowserUserAgent ラベル付与
Level 2 Scrapling Fetcher(偽装なし) JA4 または UA で検出 TGT_TokenAbsent で検出
Level 3 Scrapling Fetcher(JA4 偽装) JA4 固定なら JA4 レートルールで検出。ローテーションで回避 TGT_TokenAbsent / TGT_VolumetricIpTokenAbsent で検出
Level 4 Scrapling StealthyFetcher JA4・UA 偽装で通過 TGT_SignalBrowserInconsistency / TGT_VolumetricIpTokenAbsent で検出

検証環境

[Scrapling(公式 Docker イメージ)]

[CloudFront]
        ↓ WAF アタッチ
[Lambda Function URL](WAF SDK 埋め込み済み HTML を返す)

Lambda はテスト用の HTML を返すだけのシンプルな関数です。WAF SDK(challenge.js)を <head> に埋め込んでいます。

import os

# WAF_SDK_URL には Application Integration URL(末尾 /challenge.js を含まない)をセット
# 例: https://xxxx.edge.sdk.awswaf.com/xxxx/yyyy
WAF_SDK_URL = os.environ.get('WAF_SDK_URL', '')
SDK_SCRIPT = f'<script src="{WAF_SDK_URL}/challenge.js" defer></script>' if WAF_SDK_URL else ''

HTML = f"""<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Scrapling WAF Test</title>
  {SDK_SCRIPT}
</head>
<body>
  <h1>Scrapling WAF Test Page</h1>
</body>
</html>"""

def handler(event, context):
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "text/html; charset=utf-8"},
        "body": HTML,
    }

WAF の設定(3層のルール)は CDK で以下のように定義しています。

// WAF Web ACL(3層の多層防御)
const webAcl = new wafv2.CfnWebACL(this, 'WebAcl', {
  scope: 'CLOUDFRONT',
  defaultAction: { allow: {} },
  rules: [
    // Layer 1: IP レートルール
    {
      name: 'IpRateLimit',
      priority: 1,
      action: { block: {} },
      statement: {
        rateBasedStatement: {
          limit: 100,           // 本番では用途に応じて調整
          evaluationWindowSec: 300,
          aggregateKeyType: 'IP',
        },
      },
      // ...
    },
    // Layer 2: JA4 フィンガープリントレートルール
    {
      name: 'Ja4RateLimit',
      priority: 2,
      action: { block: {} },
      statement: {
        rateBasedStatement: {
          limit: 100,
          evaluationWindowSec: 300,
          aggregateKeyType: 'CUSTOM_KEYS',
          customKeys: [
            { ja4Fingerprint: { fallbackBehavior: 'NO_MATCH' } },
          ],
        },
      },
      // ...
    },
    // Layer 3: Bot Control Targeted(まず Count モードで観測)
    {
      name: 'BotControlTargeted',
      priority: 3,
      overrideAction: { count: {} },  // Count モードで全ルールをラベル付与のみに
      statement: {
        managedRuleGroupStatement: {
          vendorName: 'AWS',
          name: 'AWSManagedRulesBotControlRuleSet',
          managedRuleGroupConfigs: [{
            awsManagedRulesBotControlRuleSet: {
              inspectionLevel: 'TARGETED',
              enableMachineLearning: true,
            },
          }],
        },
      },
      // ...
    },
    // Layer 3 後段: ラベルマッチルールで特定ラベルのみ Block/Challenge
    // 例: TGT_TokenAbsent ラベルが付いたリクエストを Challenge
    // {
    //   name: 'BlockTokenAbsent',
    //   priority: 4,
    //   action: { challenge: {} },
    //   statement: {
    //     labelMatchStatement: {
    //       scope: 'LABEL',
    //       key: 'awswaf:managed:aws:bot-control:TGT_TokenAbsent',
    //     },
    //   },
    // },
  ],
  // ...
});

検証コード

Scrapling の公式 Docker イメージを使い、各シナリオを実行します。

from scrapling.fetchers import Fetcher, StealthyFetcher

TARGET = "https://xxxx.cloudfront.net"

# WAF ブロック判定(202: Challenge、403: Block、429: Rate limit)
def is_blocked(page):
    # WAF の CAPTCHA/Challenge 応答 HTML には aws-waf-token を設定する JS が含まれる
    # 通常ページの SDK script タグには含まれないため、この文字列の有無でブロック判定が可能
    if page.status in (202, 403, 429):
        return True
    body = str(page.body) if page.body else ''
    return 'aws-waf-token' in body or ('awswaf' in body and 'sdk.awswaf.com' not in body)

# シナリオ1: 素の HTTP リクエスト
page = Fetcher.get(TARGET)
print(f"S1: status={page.status}, blocked={is_blocked(page)}")

# シナリオ2: StealthyFetcher(Camoufox)
page = StealthyFetcher.fetch(TARGET, headless=True, network_idle=True, timeout=15000)
print(f"S2: status={page.status}, blocked={is_blocked(page)}")

# シナリオ3: IP レートループ(閾値超えで Block を確認)
for i in range(15):
    page = Fetcher.get(TARGET)
    print(f"S3-{i+1}: status={page.status}")
    if is_blocked(page):
        print(f"  -> Blocked at request {i+1}")
        break

# シナリオ4: JA4 固定で連続アクセス(閾値超えで Block を確認)
for i in range(15):
    page = Fetcher.get(TARGET, impersonate='chrome')
    print(f"S4-{i+1}: status={page.status}")
    if is_blocked(page):
        print(f"  -> Blocked at request {i+1}")
        break

# シナリオ5: impersonate ランダム変更(JA4 レート回避 → Bot Control 検出)
for i, imp in enumerate(['chrome', 'firefox', 'chrome120', 'firefox135', 'safari'] * 6):
    page = Fetcher.get(TARGET, impersonate=imp)
    print(f"S5-{i+1}: status={page.status} impersonate={imp}")
docker run --rm \
  -v $(pwd)/scripts:/scripts \
  -e PYTHONPATH=/app \
  --entrypoint python3 \
  pyd4vinci/scrapling \
  /scripts/test_fetcher.py

検証結果

シナリオ別結果

# シナリオ 結果 検出ラベル
1 Fetcher.get()(素の HTTP) ✅ Blocked token:absent + TGT_VolumetricIpTokenAbsent
2 StealthyFetcher.fetch()(Camoufox) ✅ Blocked token:absent + TGT_VolumetricIpTokenAbsent
3 IP レートループ ✅ Block(403) IpRateLimit
4 JA4 固定ループ(impersonate='chrome' ✅ Block(403) Ja4RateLimit
5 impersonate ランダム変更 ⚠️ JA4 レート部分回避 → Bot Control 検出 TGT_VolumetricIpTokenAbsent(全件)
6 DynamicFetcher(Headless Chrome) ✅ Blocked token:absent + TGT_VolumetricIpTokenAbsent
7 StealthySession(Cookie 保持) ✅ Blocked token:absent(トークン取得自体に失敗)
8 正規ブラウザ(Chrome 手動) ✅ 通過 token:id 取得確認(本番では token:accepted

注目ポイント①:JA4 レートルールの有効性

impersonate='chrome' を固定して連続アクセスすると、JA4 が同一値のため閾値を超えた時点で 403 Block されました。

[S4-1]  status=200
[S4-2]  status=200
  ...
[S4-11] status=403 -> Blocked  ← JA4 レートルール発動

注目ポイント②:JA4 ローテーションでも Bot Control が補完

impersonatechromefirefoxsafari とランダム変更すると、JA4 が変わるため JA4 レートルールは部分的に回避されます。しかし Bot Control Targeted が全リクエストに TGT_VolumetricIpTokenAbsent ラベルを付与しており、Block モードに切り替えれば全件ブロック可能です。

🔑 ここが多層防御の核心: JA4 レートをすり抜けても、Bot Control は「この IP から WAF トークンなしのリクエストが異常な頻度で来ている」という IP 単位の異常(VolumetricIpTokenAbsent)を見逃しません。JA4 レートルール(第2層)を回避しても Bot Control Targeted(第3層)が補完する、多層防御の真価がここに現れています。

[S5-1] status=403 impersonate=chrome  -> Blocked(シナリオ4で同一 JA4 の閾値を超過済み)
[S5-2] status=200 impersonate=firefox  ← JA4 レートは通過
[S5-3] status=200 impersonate=chrome120 ← JA4 レートは通過

WAF ログ(CloudWatch Logs Insights):

action=BLOCK  rule=Ja4RateLimit          ← chrome の JA4 はブロック
action=ALLOW  rule=Default_Action  labels=[token:absent, TGT_VolumetricIpTokenAbsent]
action=ALLOW  rule=Default_Action  labels=[token:absent, TGT_VolumetricIpTokenAbsent]

Bot Control が Count モードのため ALLOW になっていますが、ラベルは正確に付与されており、Block モードに切り替えれば全件ブロックできます。

注目ポイント③:StealthyFetcher(Camoufox)もトークン取得に失敗

Camoufox は Canvas/WebGL・navigator 等のブラウザ内部シグナルを偽装しますが、WAF SDK(challenge.js)のサイレントチャレンジを突破できず、aws-waf-token の取得自体に失敗しました。JS 実行環境は整っているものの、WAF が複数のブラウザ内部 API の整合性を継続的にチェックするため、ヘッドレスブラウザ特有の矛盾(タイミング特性・イベント挙動等)が検出されたと考えられます。token:absent ラベルが付与され続け、token:accepted には到達しません。


検証環境の制約と本番環境での注意点

  • 同一 IP 制約: 本検証はボットと正規ブラウザを同一 IP から実行したため、正規ブラウザも IP レートの影響を受けました。本番環境(異なる IP)では正規ブラウザは token:accepted が付与され、誤検知は発生しません。
  • レジデンシャルプロキシ: BrightData 等のプロキシサービスでリクエストごとに送信元 IP が切り替わる場合、IpRateLimitTGT_VolumetricIpTokenAbsent(IP 単位の閾値)も機能しません。この場合、WAF が頼れるのは TGT_SignalAutomatedBrowser 等のシグナル(質)のみになります。
  • WAF SDK 統合: HTML への challenge.js 追加だけでは不十分です。SPA(React/Vue 等)では fetch/XHR のインターセプターで aws-waf-token を全リクエストに付与する実装が必要です。未対応の場合、正規ユーザーが無限 CAPTCHA にハマる事故が起きます。

実運用での推奨対応フロー

追加料金が発生する Bot Control Targeted は、いきなり Block モードで有効化するのではなく、以下のステップで段階的に導入することを推奨します。

Phase 0: 初期判断(低コスト・Bot Control 不要)

  • User-Agent の異常を確認(curl, python-requests 等のデフォルト UA)
  • WAF ログの JA4 フィンガープリントで異常なリクエスト数を確認
  • CloudFront ログでサブリソース未取得パターンを確認

WAF コンソールの AI トラフィック分析ダッシュボードを使うと、AI ボット・クローラーからのアクセスパターンやリクエスト量を視覚的に把握でき、Bot Control 導入前の判断材料として活用できます。

https://dev.classmethod.jp/articles/aws-waf-ai-activity-dashboard/

💡 Tips: CloudFront ログからサブリソース未取得 IP を抽出する Athena クエリ

正規ブラウザは HTML 取得後に CSS/JS/画像を連続取得しますが、Scrapling のデフォルトは HTML のみです。

SELECT c_ip, COUNT(*) as cnt
FROM cloudfront_logs
WHERE cs_uri_stem NOT LIKE '%.css'
  AND cs_uri_stem NOT LIKE '%.js'
  AND cs_uri_stem NOT LIKE '%.png'
  AND cs_uri_stem NOT LIKE '%.jpg'
GROUP BY c_ip
HAVING COUNT(*) > 10
ORDER BY cnt DESC

Phase 1: Count モードで観測

  • Bot Control Targeted を Count モードで有効化
  • TGT_TokenAbsent / TGT_VolumetricIpTokenAbsent の出現を記録
💡 Tips: Bot Control ラベルを確認する CloudWatch Logs Insights クエリ
fields @timestamp, httpRequest.clientIp, action
| filter labels.0.name like /bot-control/
| stats count() by labels.0.name
| sort count desc

Phase 2: Block/Challenge に切り替え

  • 確信が持てたルールを Block/Challenge に変更

Phase 2.5: 例外ルール(セーフリスト)の設計

  • Bot Control ルールの前段に Allow ルールを配置
  • 「Block してから救済する」より「救済ルールを先に用意してから Block する」が原則

Scrapling 利用時の留意点

不正クロールはサイト運営者に直接的なコストを強います。WAF Bot Control Targeted は月額 $10(WebACL あたり)+リクエスト料金($10/100万リクエスト)が発生し、大量アクセスは防御コストを押し上げるだけでなく、対策強化により正規ユーザーの体験が悪化する場合もあります。

Scrapling 自体は高性能なクローラーであり、自社サービスの監視・テスト自動化・公開データの研究分析など幅広く活用できます。外部サービスに対して利用する場合は、以下を守ることを推奨します:

  • robots.txt を確認する(robots_txt_obey オプションで自動遵守できます。詳細は公式ドキュメント参照)
  • サイトの利用規約を確認する
  • リクエスト間隔を空け、過度な並列実行を避ける
  • 429・5XX が返ったら即リトライしない(429 はレート制限の明示的なシグナル。5XX はサーバー側の問題を示す可能性がある)
  • 必要最小限のデータのみ取得する

AI クローラーを開発する場合

Scrapling のアンチボット回避機能に頼るのではなく、アクセス先のサイトが認めた手段でデータを取得することが基本です。アクセス先のサイトが WBA(Web Bot Authentication) をサポートしている場合は、標準的な認証手段で正規ボットとして許可を得ることを推奨します。WBA は HTTP Message Signatures(RFC 9421)に基づく暗号署名で、AWS WAF は 2025年11月に対応、Cloudflare 等も対応済みです。

https://dev.classmethod.jp/articles/aws-waf-web-bot-auth-wba-support/


まとめ

本記事では Scrapling の各フェッチャーに対して AWS WAF の多層防御を検証しました。結果は以下の通りです。

検証項目 結果
Fetcher.get() の検出 TGT_VolumetricIpTokenAbsent で検出
StealthyFetcher の検出 ✅ トークン取得失敗を確認
JA4 レートルールの有効性 impersonate='chrome' 固定で Block 確認
JA4 ローテーション時の補完 ✅ Bot Control Targeted が全件検出
多層防御の確認 ✅ JA4 レートルールを回避しても Bot Control Targeted が補完

本検証では、AWS WAF の多層防御が Scrapling の高度なアンチボット回避機能に対しても有効に機能することを確認しました。特に Bot Control Targeted の TGT_VolumetricIpTokenAbsent は、JA4 レートルールを回避した場合でも検出できました。

Scrapling のような高度なクローラーによるボット被害が出ている場合は、AWS WAF のチューニングをご検討ください。特に Bot Control Targeted は、TLS フィンガープリント偽装を駆使するツールに対しても、JS 実行環境レベルで検出できる強力な選択肢です。


参考リンク

この記事をシェアする

関連記事