短時間に連続送信してくるIPに 制限をかける

以前からCSRFでなりすまし対策の記述、前記事でのreCAPTHAを使ってbot対策は出来たけれど、

人海戦術での嫌がらせの場合は対応できない。

なので、短時間に大量の送信があった場合にIP制限をかけることにしました。

流れとしては

  1. IPレート制限チェック
  2. CSRFチェック
  3. reCAPTCHA検証
  4. バリデーション
  5. メール送信
  6. IPログ保存
  7. Thanks画面へリダイレクト

の順序で動作させるのがベストかと思うので、1と6を作っていきます。

目次

MySQLでIP管理

まず、ルールとして同一IPから5分以内に3回以上メール送信があった場合ブロックすることとします。

また、レンタルサーバーでも確実に動くため、MySQLを使ってIP管理をしようと思います。

SQL
CREATE TABLE contact_rate_limits (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  ip_address VARCHAR(45) NOT NULL,
  created_at DATETIME NOT NULL,
  INDEX idx_ip_time (ip_address, created_at)
);

IPv6対応のため ip_address は VARCHAR(45)にしておきました。

また、時間検索用に index を貼ります。

IPレート制限チェック

Controllerの送信処理の一番上に下記を記述します。

特にreCAPTCHAはGoogleとのAPI通信をしているため、

無駄な API 通信を防ぐためにも一番上に記述するのが適当かと思います。

Controller
$ip = $_SERVER['REMOTE_ADDR'];

// 直近5分の送信回数を取得
$stmt = $pdo->prepare(
  'SELECT COUNT(*) FROM contact_rate_limits
   WHERE ip_address = ?
     AND created_at >= (NOW() - INTERVAL 5 MINUTE)'
);
$stmt->execute([$ip]);
$count = (int)$stmt->fetchColumn();

if ($count >= 3) {
  exit('短時間に送信が集中しています。しばらく時間を置いてからお試しください。');
}

REMOTE_ADDR というのがアクセス元のIPアドレスになります。

IPログ保存

メール送信が成功した後に下記を記述します。

Controller
$stmt = $pdo->prepare(
  'INSERT INTO contact_rate_limits (ip_address, created_at)
   VALUES (?, NOW())'
);
$stmt->execute([$ip]);

DB接続に嵌まる

何故か Undefined variable $pdo が出てしまう。

それじゃあDB操作なんて何もできないよ。

調べたら、管理画面で流用していたDB接続用のPHPファイルがうまく読み込めていなかった。

それを読み込むだけじゃダメだったので、$pdo自体を返すように改変した。

public/config.php
<?php
$host = 'localhost:ポート番号';
$db   = 'DB名';
$user = 'ユーザー名';
$pass = 'パスワード';
$dsn  = "mysql:host=$host;dbname=$db;charset=utf8mb4";

try {
    return $pdo = new PDO($dsn, $user, $pass, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ]);
} catch (Exception $e) {
    echo 'DB接続エラー: ', $e->getMessage();
    exit;
}
?>

$pdo の前に return を書くだけでOK!

また、Controllerには下記を記述してグローバル変数を減らした。

Controller
$pdo = require __DIR__ . '/../../public/config.php';

無事に書き込まれました!(なんかIPアドレスおかしいけど)

定期的にDBに書き込まれたIPアドレスを削除

アクセスされる度にログはどんどん溜まっていくので、定期的に削除処理が必要になります。

ということで、1日以上経ったログは削除する方法を追記します。

削除用のPHPファイルの作成

Jobs/cleanup_contact_rate_limits.php
<?php
$pdo = require __DIR__ . '/../../public/config.php';

// 1日以上前のログを削除
$stmt = $pdo->prepare(
  'DELETE FROM contact_rate_limits
   WHERE created_at < (NOW() - INTERVAL 1 DAY)'
);

$deleted = $stmt->execute();

// cronログ用
echo date('Y-m-d H:i:s') . " cleanup done\n";
?>

とりあえずこれでよし。

タスクスケジューラに登録

筆者はWindows11ユーザーなので、その方法をご紹介します。

まずスタートボタンから「タスクスケジューラ」と入力して、アプリを表示します。

カテゴリを「すべて」にすると、ブラウザで検索されてしまうことがあるので、

「アプリ」ボタンを押下することをお勧めします。

タスクスケジューラが表示されたら、右上の基本タスクの作成を押下します。

トリガーは下記のように設定しました。

  • 毎日
  • 間隔:1日
  • プログラムの開始

プログラムには下記を設定します。

  • (php.exeがあるフォルダ)/php.exe

続いて、引数には下記を設定します。

  • (PHPフォルダ)/Jobs/cleanup_contact_rate_limits.php

どちらも絶対パスで入力してください。

「開始」は省略して大丈夫です。

DB自体にイベントスケジューラを登録する場合

DB自体にイベントスケジューラを登録することもできます(これの方が現実的かも)

DBをクリック後、右上の方に イベントボタン が表示されるので、それをクリック。

表示されたフォームに下記を記述。

  • イベント名:お好きな名前で
  • 状態:ENABLED
  • イベント種別:RECURRING
  • 実行間隔:1 DAY
  • 開始、終了は空白でOK
  • 定義:前述した下記のSQL
SQL
DELETE FROM contact_rate_limits
   WHERE created_at < (NOW() - INTERVAL 1 DAY)

これを設定すれば、メールが送られた1日後にはレコードが削除されます。

  • URLをコピーしました!
目次