登録フォームのサブスク爆撃を防ぐ方法
攻撃者は1つのメールアドレスを数百の登録フォームへ送信します。confirmed opt-inとrate limitで、確認前のメール送信を止める方法を解説します。
サブスクリプション爆撃とは
サブスクリプション爆撃は、登録フォームを使って他人の受信トレイを大量のメールで埋める攻撃です。攻撃者は 1 人の被害者のメールアドレスを取得し、短時間のうちに、保護されていない数百から数千のフォームへ送信します。各サイトは、そのアドレス宛てに歓迎メッセージや確認メッセージを送信します。これらのメッセージによって、被害者が本当に読む必要のあるメールが埋もれます。
標的は、その受信トレイを所有する人です。サブスクリプションの確認メールで受信トレイが埋まっている間に、攻撃者はその人のカードで支払いを行ったり、その人のアカウントのパスワードをリセットしたりします。銀行からの不正利用警告も届きます。しかし、同じ 1 時間に届いた他の 2000 通のメッセージの下に埋もれるため、誰にも間に合うように確認されません。
攻撃者は、あなたのサーバーを攻撃に利用します。サーバー上のソフトウェアが壊れているわけではありません。あなたのアカウントが侵害されたわけでもありません。誰かが公開フォームにアドレスを入力し、あなたのソフトウェアは設計どおり、そのアドレスにメールを送信しただけです。これが、この攻撃を発見しにくい理由です。侵入はなかったため、ログに侵入の痕跡はありません。
攻撃がこちら側からどのように見えるか
攻撃には、2 つの形があります。
目立つ形は、短時間に集中するバーストです。数分の間に、1 つのフォームへ数百件の POST リクエストが届きます。送信元 IP アドレスは多数に分散しており、宛先には、これまで送信したことのないドメインのアドレスが含まれています。確認すれば、すぐに気付けます。
見逃されるのは、目立たない形です。攻撃者は、脆弱なフォームを数千件まとめたリストを持っています。そのため、あなたのフォームには 1 時間あたり 1 件か 2 件の送信が発生するだけで済みます。Jye Cusch は、運営しているサイトでまさにこの形の攻撃を受けたことを説明しています。トラフィックの急増はありませんでした。対象者の利用時間帯とは合わない時間に、一定したサインアップが届いていただけです。1 件のフォームだけを見ると、ほとんど処理していないため、問題がないように見えます。被害の大きさは、攻撃者のリストにあるすべてのフォームを合計したものです。
どちらの形にも、発生後に共通する特徴があります。その後、何も起こりません。アドレスは確認されず、メッセージが開かれることも、リンクがクリックされることもありません。confirmed opt-in のリストでは、ステータス unconfirmed のままになります。この滞留が、得られる最も明確な証拠です。
まず、access log で 1 分あたりの送信件数を数えます。
sudo awk '/POST \/subscription\/form/ {print substr($4, 2, 17)}' \
/var/log/nginx/access.log | uniq -c | sort -rn | headデフォルトの combined log format では、$4 が角括弧で囲まれたタイムスタンプです。そのため、このコマンドは分ごとの件数を多い順に表示します。通常は 1 日 4 件のサインアップしかないフォームで、1 分間に 60 件表示されたなら、正常な状態ではありません。
確認オプトイン:最も大きな効果がある防御策
通常はダブルオプトインとも呼ばれる確認オプトインでは、そのアドレスに送信されたメッセージ内のリンクを本人がクリックするまで、そのアドレスは購読者になりません。有効にすると、1 件の送信アドレスから送られるメッセージは、最初の 1 通だけになります。そのアドレスはリストに追加されないため、キャンペーンもウェルカムシーケンスも受信しません。
セルフホスト型ニュースレターサーバーの listmonkでは、これはリスト単位の設定です。リストはシングルオプトインまたはダブルオプトインのいずれかになります。ドキュメントでは、この違いを明確に説明しています。ダブルオプトインのリストでは、購読者は「受信した確認メールのリンクをクリックして、購読を明示的に承認します。それまではキャンペーンメッセージを受信しません」。購読者は unconfirmed の状態にあり、クリックすると confirmed に移行します。オプトインリストでキャンペーンメールを受信するのは、confirmed の購読者だけです。
この仕組みで得られる効果を正確に把握してください。確認オプトインを使っても、あなたの寄与がゼロになるわけではありません。アドレスごとに 1 通までに抑えられるだけです。被害者はその 1 通を受信します。また、1,000 個のサイトからそれぞれ 1 通ずつ送られれば、それだけで攻撃全体になります。確認オプトインによって防げるのは、その後に続くすべての送信です。リストはクリーンな状態に保たれ、最初の 1 通を求めていなかった相手に 2 通目を送ることもありません。
さらに重要な設定が 2 つあります。どちらも忘れやすい設定です。1 つ目は、確認メールの再送回数を制限することです。同じアドレスを再び送信でき、そのたびに別の確認メールが送られる場合、攻撃者は 1,000 個のフォームを用意する必要がありません。フォームだけで 1,000 通を送信できるからです。そのリストですでに unconfirmed の状態にあるアドレスには、少なくとも 1 日は追加のメールを送らないようにします。2 つ目は、未確認の行を定期的に削除することです。30 日間確認されていないアドレスは、保留中の購読者ではありません。残しておくと、後で誤ってメールを送信する可能性が生じるだけです。
リバースプロキシでサインアップフォームをレート制限する
制限はアプリケーション内部ではなく、その前段に配置します。プロキシでブロックされたリクエストは、データベース接続を開かず、SMTP(simple mail transfer protocol)の通信も開始しません。アプリケーション内部の制限は、リクエストによってすでにワーカープロセスとクエリを消費した後に実行されます。また、多くの構成では、不正利用のチェックが実行される前にメッセージがキューへ追加されます。プロキシの制限は、置き換えたコード内に存在しないため、アプリケーションをアップグレードしても維持されます。
以下の例では nginx を使用します。考え方は、アプリの前段で稼働させているリバースプロキシにも適用できますが、ディレクティブ名は異なります。
http ブロック内の、たとえば /etc/nginx/conf.d/signup-limit.conf というファイルに次の内容を記述します。
map $request_method $signup_key {
POST $binary_remote_addr;
default "";
}
limit_req_zone $signup_key zone=signup:10m rate=2r/m;
limit_req_status 429;
limit_req_log_level warn;map が実際の処理を担います。nginx はキーが空文字列のリクエストをカウントしないため、POST リクエストだけがゾーンに入ります。サインアップページを何度読み込んでも、利用枠は消費されません。この map がない場合、ページを 2 回更新しただけで、送信前に自分の利用枠を消費してしまいます。
$binary_remote_addr はクライアントアドレスをパックした形式です。そのため、10 megabyte のゾーンには約 160,000 個のアドレスを格納できます。rate=2r/m は 30 秒に 1 回の送信を許可します。limit_req_status 429 は nginx のデフォルトである 503 ではなく、HTTP 429 Too Many Requests を返します。これは適切なステータスコードであり、クライアントライブラリが想定するコードでもあります。
次に、サイトの server ブロックに次の内容を追加します。
location = /subscription/form {
limit_req zone=signup burst=3 nodelay;
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}burst=3 nodelay により、ボタンをダブルクリックした場合のリクエストは通過し、4 回目のリクエストはキューに入れず即座に拒否されます。
sudo nginx -t && sudo systemctl reload nginxnginx -t は configuration file /etc/nginx/nginx.conf test is successful を出力するはずです。次に、フォームを短時間で 5 回送信し、エラーログを監視します。
sudo tail -f /var/log/nginx/error.logブロックされたリクエストは 1 行を出力します。探す文字列は次のとおりです。
2026/08/13 09:14:22 [warn] 812#812: *4412 limiting requests, excess: 3.400 by zone "signup", client: 203.0.113.10, server: news.example.com, request: "POST /subscription/form HTTP/1.1", host: "news.example.com"行がまったく出力されない場合、制限は適用されていません。よくある原因は、limit_req が、リクエストが到達しない location ブロック内に置かれていることです。そのため、curl -si -X POST https://news.example.com/subscription/form を数回連続して実行し、429 が得られることを確認します。
IP アドレス単位の制限を使用する前に、知っておくべき注意点が 2 つあります。
CDN または別のプロキシの背後では、$binary_remote_addr はそのプロキシになります。 すべての訪問者が 1 つのバケットに入るため、各分の最初の数件の送信によって、他のすべての訪問者が拒否されます。実 IP モジュールを使用して修正します。CDN が公開している各範囲に対して set_real_ip_from を設定します(Cloudflare の範囲は cloudflare.com/ips に掲載されています)。その後、real_ip_header CF-Connecting-IP を設定します。アクセスログで $remote_addr を読み取り、CDN のアドレスではなく訪問者のアドレスになっていることを確認して、修正を検証します。
IPv6 では、アドレス単位の制限は弱くなります。 $binary_remote_addr は完全な /128 を保持します。一般的な家庭向け IPv6 の割り当ては /64 以上です。これは攻撃者が使えるアドレス数を大きく上回り、各アドレスに新しい利用枠が設定されます。エンドポイント自体に定数をキーとして第 2 のゾーンを追加し、上限として使用します。これにより、送信元アドレスがいくつあっても、フォーム全体のレートを制限できます。
map $request_method $signup_total_key {
POST "signup";
default "";
}
limit_req_zone $signup_total_key zone=signup_total:1m rate=30r/m;同じ location に limit_req zone=signup_total burst=10 nodelay; を追加します。実際の利用が最も多い時間帯のレートを上回る値にし、十分な余裕を持たせます。これは大まかな制御です。攻撃中は、正当なサインアップも拒否されます。この選択が適切です。別の方法では、サーバーがメールを送信し続けることになるためです。
プロキシでアドレス単位の制限を適用できない理由
メールアドレスは POST body 内に含まれますが、nginx は request body を解析しません。limit_req_zone がキーにできる変数は、request line、headers、connection から取得されるものだけです。そのため、「このアドレスには 1 日あたり最大 1 件の確認メールだけを送信できる」というルールは、body を最初に読み取るコンポーネント、つまりアプリケーションに配置する必要があります。
アドレスを query string に移して $arg_email を利用可能にする方法で回避してはいけません。これにより、すべての購読者のアドレスが access log に平文で記録され、その下流にある任意の log shipper にも渡ります。レート制限と引き換えに、プライバシーの問題を招くことになります。
実際の例外は 1 つあります。nginx JavaScript module の njs は request body を読み取り、そこから変数を設定できます。これにより、プロキシでアドレス単位のキーを作成できます。これは実用的な選択肢ですが、request path に新しいコードを追加することにもなります。多くのサイトでは、アドレス単位の上限は、そのアドレスに保留中の確認があるかをすでに把握している database の近くに配置するのが適切です。一方、プロキシでは、得意とする IP 単位および endpoint 単位の制限を処理します。
送信された内容をメッセージに再掲しない
攻撃者が入力した文字列を、送信するメッセージに含めないでください。これには、実際に悪用されてきた2つの理由があります。
確認メールでフォームから取得した名前を使って宛先を呼びかけると、攻撃者は名前欄に任意のメッセージを書き込めます。するとサーバーは、その文字列をあなたのドメインから、あなたの DKIM(DomainKeys Identified Mail)鍵で署名して被害者に配信します。サイトが他者による悪用の配信サービスになり、受信側プロバイダーにはあなたのドメインが表示されます。
2つ目の理由は、さらに深刻です。送信されたフィールドを手作業でメールヘッダーに連結すると、そのフィールド内の改行文字によって、攻撃者が選んだヘッダーを追加できます。その中には Bcc も含まれます。最新のメールライブラリは、ヘッダー値内の改行を拒否します。しかし、シェルスクリプトから sendmail にテキストをパイプするコードでは、拒否されないことがよくあります。
安全な確認メッセージには、サイト名と1つのリンク、および1文の説明だけを含めます。アドレス自体は、メール転送エージェントが必要とする To ヘッダーにだけ記載します。次の方法でテストします。改行文字と明らかなリンクを含む名前を入力してフォームを送信し、less で受信した生メッセージを読みます。どちらも残っていないことを確認してください。
この際、成功ページの内容がどのアドレスでも同じになるようにします。あるアドレスには「すでに購読済みです」と表示し、別のアドレスには「受信トレイを確認してください」と表示するページは、アドレス一覧を持つ人物にとって、フォームを登録状況の確認手段に変えてしまいます。
どのボットチェックを使用すべきですか?
有効性だけでなく、アクセシビリティも同じように重視して選んでください。画像選択式 CAPTCHA は視覚障害のある読者には解けず、音声による代替手段も、一般的な聴力の人には利用しにくい場合があります。正当な利用者の登録を妨げるチェックは、防御策であると同時に利用者への負担でもあります。試す順番に 4 つ挙げます。
ブラウザーでの Proof of work。 ブラウザーがハッシュを計算し、サーバーはそれを低コストで検証できます。利用者が解く課題はありません。listmonk では Settings、次に Security から、第三者サービスを必要としない ALTCHA を使用できます。2026 年 8 月時点で、listmonk は非推奨となった hCaptcha よりも ALTCHA を推奨しています。最も多く送信する者に負担がかかるため、コストを負担するのは攻撃者です。
管理された非対話型チェック。 Cloudflare Turnstile は、シグナルに問題がある場合だけチャレンジを表示し、ほとんどの訪問者には何も表示しません。有効ですが、登録処理に第三者を組み込むことになります。
ハニーポットフィールド。 利用者には表示されず、単純なボットだけが入力するテキスト入力欄です。フォームで他に使用していない名前を付け、パスワードマネージャーが入力せず、スクリーンリーダーが読み上げないように autocomplete="off"、tabindex="-1"、aria-hidden="true" を設定します。email2 または address という名前のフィールドはブラウザーによって自動入力されるため、正当な利用者を拒否することになります。
<div style="position:absolute; left:-9999px;" aria-hidden="true">
<label for="hp_ref">Leave this field empty</label>
<input type="text" id="hp_ref" name="hp_ref" autocomplete="off" tabindex="-1">
</div>送信までの時間によるチェック。 ページを表示した時点で署名付きタイムスタンプを hidden フィールドに設定し、2 秒未満で到着した送信を拒否します。人がフォームを読み、アドレスを入力して送信するには速すぎます。タイムスタンプには署名してください。署名しないと、ボットは古いタイムスタンプをそのまま送信できます。
どれを選んでも、1 点確認すべきことがあります。トークンは 1 回だけ使用できなければなりません。スクリプトがチェックを 1 回だけ解決し、そのトークンを 1000 件のアドレスに対して再利用できるなら、そのチェックで証明できるのはブラウザーが 1 回実行されたことだけです。
不正利用の報告が届く前に、どのように把握できますか?
ホスティングプロバイダーの abuse 対応窓口ではなく、自分のグラフで異常を把握できるようにします。確認する点は 2 つです。
ログ全体で送信元アドレスごとの送信数を集計します。
sudo awk '/POST \/subscription\/form/ {print $1}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20次に、nginx がすでに書き込んでいる同じ limiting requests 行を fail2ban に読み込ませ、繰り返し送信するアクセス元を ban します。fail2ban にはこの用途専用の filter が用意されています。/etc/fail2ban/jail.d/nginx-limit-req.local を作成します。
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
port = http,https
logpath = /var/log/nginx/error.log
findtime = 600
maxretry = 10
bantime = 3600sudo systemctl reload fail2ban
sudo fail2ban-client status nginx-limit-reqstatus の出力には、jail の filter と、現在の failed 数および banned 数が表示されます。静かな日に Currently banned: 0 となるのは正常です。jail 自体が表示されない場合、fail2ban はそのファイルを読み込めていません。sudo fail2ban-client -d | grep nginx-limit-req で、実際に解析した設定を出力できます。標準の filter はすべての limit_req zone に一致します。/etc/fail2ban/filter.d/nginx-limit-req.local の [Definition] セクションで ngx_limit_req_zones = signup を設定し、signup zone だけに絞り込みます。jail ファイルの構成と ban コマンドについては、Ubuntu 24.04 向け fail2ban ガイドで詳しく説明しています。
2 つ目の指標は比率で、新しいソフトウェアは必要ありません。送信数を確認数で割ります。正常な list では、アドレスを送信した人の大半がリンクをクリックします。通常は半数を大きく上回ります。送信数が増えているのにこの比率が急落した場合、システムが不正利用されています。すでにレポートを実行しているスケジュールに合わせて、直近 1 時間に作成された unconfirmed subscribers の数と、confirmed された ones の数を比較します。
発生する影響: 送信者レピュテーションとブロックリスト
ここで、単なる迷惑行為が請求につながります。
大量送信に使われるアドレスリストは収集されたものです。収集リストには、スパムトラップも含まれます。スパムトラップは、どこにも登録されておらず、許可なくメールを送信する送信者を検出するためだけに公開されたアドレスです。確認メッセージがそのアドレスに届くと、それだけで十分と判断するブロックリスト運営者もいます。
メッセージを要求していない受信者は、配信停止をクリックしません。「迷惑メールを報告」をクリックします。2024 年 2 月から適用されている Google の一括送信者向けルールでは、Gmail に 1 日 5,000 件以上のメッセージを送信する送信者に対し、Postmaster Tools で報告された迷惑メール率を 0.3% 未満に維持するよう求めています。送信数が少ない送信者はこの数値による判定対象ではありません。ただし、同じ苦情シグナルがフィルタリングの判定に使われ、メールが迷惑メールフォルダに振り分けられる原因になります。実行時に使われた偽のアドレスはハードバウンスも発生させます。ハードバウンス率の上昇は、大手プロバイダーではそれ自体がレピュテーションシグナルになります。
mailcow を使用して VPS 上で独自のメールサーバーを運用している場合、登録対象になるのは IP アドレスとドメインです。Spamhaus のような運営者に登録解除を申請するには、フォームの送信と待機が必要です。待機中は、請求書やパスワードリセットのメールも配信されません。共有プロバイダー経由で送信する場合は、まずアカウントを停止され、その後で説明を読まれると考えてください。あなたのネットワークトラフィックが、その IP の他のすべての送信者にとってリスクになるためです。
これに対して、必要な作業は少量です。まず今日、confirmed opt-in を有効にします。リストごとに 1 つの設定で済むためです。次に、プロキシのレート制限を追加します。1 つのファイルを変更して reload するだけです。bot check とアラート設定は、今週中に続けて実施できます。
FAQ
ダブルオプトインでサブスクリプション爆撃を防げますか?
リストの汚染を防ぎ、送信元として関与するメッセージを、送信されたアドレスごとに 1 通までに抑えられます。これは実施できる対策の中で、単独では最大の改善です。ただし、攻撃は 1000 件のサイトからそれぞれ 1 通ずつ送られるため、被害者の受信トレイが埋まることは防げません。プロキシで IP ごとのレート制限を設定し、確認メールの再送回数にも上限を設けてください。同じアドレスが 2 回送信されても、2 通目のメッセージが生成されないようにします。
爆撃と、実際の登録が多い正常な日をどう見分けますか?
送信後の動きを確認します。実際の登録では確認操作が行われ、通常は数時間以内に確認されます。爆撃では、確認されず、開封もクリックもされないアドレスが大量に残ります。送信元アドレスにも不自然な集中が見られます。これまで見たことのないアドレスが多数含まれ、普段は送信しない宛先ドメインが多く、到着時刻も利用者の活動時間に沿わず、1 日全体に均等に分散します。
送信されたアドレスを削除すべきですか?
はい。約 30 日以上確認されていないレコードを削除し、手作業ではなくスケジュールで実行してください。これらのアドレスには、謝罪や「これはあなたですか?」というメッセージも含め、他のメッセージを絶対に送信しないでください。すでに大量のメッセージを受け取っている相手に、要求されていない 2 通目を送ることになるためです。これらのアドレスに spamtraps が含まれていた場合、追跡メッセージは blocklist の運営者が待っている確認材料になります。
レート制限によって実際の購読者が拒否されませんか?
IP ごとに 30 秒あたり 1 件、burst を 3 件とする制限であれば、フォームに 1 回入力する利用者には気付かれません。1 つのアドレスを多くの利用者が共有している場合は影響が出ます。たとえば、1 台の NAT (network address translation) ゲートウェイの背後にあるオフィスが該当します。また、プロキシから訪問者ではなく CDN のアドレスが見える場合にも同様です。制限を厳しくする前に、アクセスログで $remote_addr を確認してください。エンドポイントの上限は、実際の利用が最も多い時間帯を上回る値に維持します。
爆撃後に送信元 IP が blocklist に登録されました。最初に何をすべきですか?
何かを申請する前に、その IP からの送信を停止してください。キャンペーンキューを一時停止し、フォームを修正して、確認されていないアドレスを削除します。同じトラフィックを送信し続けると、登録解除後に最初より速く再登録されるためです。次に、どのリストに登録されているかを確認します。多くの運営者は IP アドレスをキーにした検索ページを提供しています。そこで指定された削除手順に従ってください。対応には数日かかると考え、その間に SPF (sender policy framework) レコードと DKIM 署名が現在も正常に検証されることを確認します。