IPアドレスの匿名化:個人情報を保存せずに攻撃をログする方法
2023年5月、フィンランドのデータ保護当局は、有効な法的根拠なしにセキュリティログに生のIPアドレスを保存していた企業に75万ユーロの罰金を科しました。企業側は、ログが侵入検知に必要だと主張しました。規制当局はその前提に反対したわけではありません。反対したのは実装方法でした。ログにはハッシュ化されていない完全なIPアドレスが含まれており、GDPRの下では個人データの処理操作に該当したのです。
これはすべての開発者とシステム管理者が直面するジレンマです。システムを防御するために攻撃データが必要ですが、そのデータを保存することで法的責任が生じる可能性があります。 個人情報保護を行わないセキュリティログは、コンプライアンスの地雷原です。ログを一切取らなければ、盲目的に運用することになります。すべてを生データで記録すれば、次の監査で罰金を受ける一歩手前です。
中間の道があります。ローテーションするソルトを使ってIPアドレスをハッシュ化することで、パターンの検出やリピートオフェンダーの特定能力を維持しながら、元のアドレスを復元することを数学的に不可能にできます。この記事では、PHPでその実装方法を具体的に説明します。WordPressプラグインやその他のサーバーサイドアプリケーションにそのまま組み込める、本番環境対応のコードを提供します。
問題点:セキュリティログ vs. プライバシー規制
IPアドレスは個人データである
これはEUにおいて確立された法律です。欧州司法裁判所はBreyer対ドイツ(C-582/14、2016年)において、動的IPアドレスは個人データに該当すると判決を下しました。ただし、処理を行う事業者がアドレスの背後にいる自然人を特定する法的手段を有する場合に限ります。フォーム送信やユーザーアカウント、ISPとの協力でIPアドレスをクロスリファレンスできるほとんどのウェブサイト運営者にとって、このしきい値は満たされています。
GDPR第4条(1)では、個人データとは「識別されたまたは識別可能な自然人に関するあらゆる情報」と定義されています。IPアドレスにタイムスタンプとURLを組み合わせれば、個人を特定するのに十分です。
それでもログは必要
セキュリティログなしでは、以下のことができません:
- 複数のフォーム送信やログイン試行にわたるブルートフォースパターンの検出。
- 同一ソースから時間をかけて発生する攻撃の相関分析。
- 悪意のあるトラフィックを生成しているネットワークやASNに関する脅威インテリジェンスの生成。
- 不正利用への対処方法を問う規制当局、クライアント、パートナーへの適正なデューデリジェンスの証明。
問題は、ログを取るかどうかではありません。何をログに取るかです。
コンプライアンスの罠
ほとんどのWordPressセキュリティプラグインは、以下の2つのまずいアプローチのいずれかを取っています:
- すべてを生データで記録する。 完全なIPアドレス、ユーザーエージェント、タイムスタンプ、リファラーURL――すべてを
wp_optionsやカスタムテーブルに無期限に保存します。これはGDPR違反が起きるのを待っている状態です。 - 何も記録しない。 一部のプラグインはリクエストメタデータを一切記録せず、問題を完全に回避します。これはプライバシー要件を満たしますが、協調攻撃やリピートオフェンダーの検出ができなくなります。
どちらのアプローチも不十分です。個人情報保護とは有用なデータを破壊することではありません。データを変換して、個人を特定できないようにしながら運用上有用な状態を維持することです。
技術的詳解:ローテーションソルトによるIPアドレスのハッシュ化
コアコンセプト
203.0.113.42を保存する代わりに、a1b7f2e9...c3d1を保存します。ハッシュは決定論的です――同じIPアドレスは指定された時間枠内で常に同じハッシュを生成します――したがって、リピートオフェンダーの検出は可能です。しかし、ハッシュは不可逆です。ハッシュ出力から203.0.113.42を復元することはできません。つまり、保存された値はGDPRの大半の解釈において個人データではなくなります。
主要なメカニズムはローテーションソルトを用いた鍵付きハッシュです:
identifier = sha256(ip_address + daily_salt)
デイリーソルトは24時間ごと(または選択した任意の期間)にローテーションします。これにより:
- 同じ日の中では、同じIPアドレスは常に同じ値にハッシュされます。そのソースがフォームに何回アクセスしたか数えたり、パターンを検出したり、リピートオフェンダーにフラグを立てたりできます。
- 日をまたぐと、ハッシュが変わります。ハッシュ値だけでは、月曜日の攻撃者と火曜日の攻撃者を関連付けることはできません。これにより再識別リスクが制限されます。GDPRが重視するのはまさにこの点です。
なぜ単なる切り詰め(トランケーション)ではだめなのか
一般的な代替手段として、IPの切り詰め(最後のオクテットをゼロに置き換える:203.0.113.0)があります。Google Analyticsもこの方式を使用しています。シンプルですが、実際には以下の制約があります:
- 衝突率が高い。 /24ブロックには256個のアドレスが含まれます。
203.0.113.0に切り詰めると、256の異なるソースを区別できなくなります。セキュリティログにとって、この精度では粗すぎることがよくあります。 - 残りのオクテットだけでも部分的に識別可能。 切り詰めたIPアドレスにタイムスタンプとジオロケーションデータを組み合わせると、GDPRの下で個人データに該当する可能性があります。
- 実際にはリバーシブル。 切り詰めたIPとおおよその時間枠がわかれば、検索空間はわずか256アドレスです。これは簡単にブルートフォースで解読可能です。
ソルトを用いたハッシュ化は、これら3つの問題をすべて回避します。出力は固定長で、均一に分散され、ソルトなしでは計算上リバースが不可能です。
ローテーションソルトが重要な理由
静的ソルト(変更されないソルト)は、IPアドレスとハッシュ値の間に永続的なマッピングを作成します。もしソルトが漏洩した場合――データベースの侵害、バックアップの流出、内部者の脅威を通じて――攻撃者はIPv4アドレス空間全体(約43億エントリ)のレインボーテーブルを事前計算できます。最新のハードウェアであれば、そのテーブルは数時間で構築可能です。
ローテーションソルトは被害範囲を限定します。ある日のソルトが漏洩しても、その日のハッシュのみが脆弱になります。それ以前とそれ以降の日は保護されたままです。
ローテーション期間はトレードオフです:
| ローテーション期間 | パターン検出ウィンドウ | 再識別リスク |
|---|---|---|
| 1時間 | 非常に短い | 非常に低い |
| 24時間 | ほとんどの攻撃パターンに十分 | 低い |
| 7日間 | スローアンドロー攻撃に適している | 中程度 |
| なし(静的) | 無制限 | 高い |
ほとんどのWordPressセキュリティログのユースケースでは、24時間が最適なバランスです。1日を通してコンタクトフォームを連打しているボットを検出するのに十分な時間がありつつ、長期的なトラッキングに使用される可能性のある永続的な識別子を作成しません。
解決策:本番環境対応のPHP実装
基本的なハッシュ化IP関数
最小限の実装は以下の通りです。IPアドレスを受け取り、秘密鍵から派生したデイリーソルトと組み合わせて、SHA-256ハッシュを返します。
/**
* IPアドレスからプライバシーに配慮した識別子を生成する。
*
* 日次ローテーションソルトを使用し、同じIPアドレスは
* 24時間のウィンドウ内で同じハッシュを生成する。
* これにより、個人情報を保存せずにパターン検出が可能。
*
* @param string $ip 生のIPアドレス(v4またはv6)。
* @param string $secret サイト固有の秘密鍵(WPではwp_salt()を使用)。
* @param string $period オプション。日付ベースのローテーションキー。デフォルト:今日の日付。
* @return string 64文字の16進数文字列(SHA-256)。
*/
function anonymize_ip( string $ip, string $secret, string $period = '' ): string {
if ( $period === '' ) {
$period = gmdate( 'Y-m-d' );
}
// IPv6を完全形に正規化し、表記の揺れを回避
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
$ip = inet_ntop( inet_pton( $ip ) );
}
$salt = hash_hmac( 'sha256', $period, $secret );
return hash( 'sha256', $ip . $salt );
}
使い方はシンプルです:
$secret = defined( 'AUTH_KEY' ) ? AUTH_KEY : 'fallback-secret-change-me';
$hashed = anonymize_ip( $_SERVER['REMOTE_ADDR'], $secret );
// 結果: "a1b7f2e9d4c8...f3e1"(64文字の16進数)
// ログテーブルに保存
$wpdb->insert( $log_table, [
'source_hash' => $hashed,
'event_type' => 'spam_submission',
'created_at' => current_time( 'mysql', true ),
] );
主要な設計判断の解説
ソルト派生になぜhash_hmacを使うのか? HMACを使用してシークレットキーからデイリーソルトを派生させることで、長さ拡張攻撃を防止し、日付文字列が予測可能であっても(実際に予測可能です――誰でも今日の日付を知っています)ソルトが均一にランダムになることを保証します。
シークレットになぜAUTH_KEYを使うのか? WordPressはインストール時に一意のAUTH_KEYを生成し、wp-config.phpに保存します。サイト固有であり、十分にランダムで、ファイルパーミッションによってすでに保護されています。別のシークレットを管理する必要がなくなります。
なぜSHA-256なのか? リクエストごとの計算に十分な速さがあり、PHP拡張なしで広くサポートされており、256ビットの出力で実用的な衝突リスクを排除します。ここではbcryptやArgon2は不要です。それらは遅いことを意図して設計されており、パスワードハッシュには望ましいですが、リクエストごとのログには逆効果です。
リピートオフェンダーの検出
ハッシュは24時間のウィンドウ内で決定論的なため、ログから同一ソースの繰り返しをクエリできます:
/**
* 今日のウィンドウ内で同じ匿名化ソースからのイベント数をカウントする。
*
* @param string $ip 生のIPアドレス。
* @param string $secret サイト固有のシークレット。
* @param string $table ログテーブル名。
* @return int 今日のこのソースからのイベント数。
*/
function count_source_events( string $ip, string $secret, string $table ): int {
global $wpdb;
$hashed = anonymize_ip( $ip, $secret );
$today = gmdate( 'Y-m-d 00:00:00' );
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$table}
WHERE source_hash = %s AND created_at >= %s",
$hashed,
$today
)
);
}
// 例:今日20件を超える送信でブロック
$attempts = count_source_events( $_SERVER['REMOTE_ADDR'], $secret, $log_table );
if ( $attempts > 20 ) {
wp_die( 'Rate limit exceeded.', 429 );
}
これにより、データベースに個人情報を一切保存することなく、効果的なレート制限とパターン検出が実現します。規制当局が特定の個人に関連するすべての個人データの提出を求めた場合、IPアドレスを保存していないと正直に回答できます。ハッシュはリバースできず、ソルトは日次でローテーションするため、ローテーションウィンドウが過ぎた後は、あなた自身でさえどのハッシュがどのIPに属するか特定できません。
ログ保持の管理
ハッシュ化されたデータであっても、永久に保存すべきではありません。古いエントリを自動削除する保持ポリシーを実装してください:
/**
* 保持期間を超えた匿名化ログエントリを削除する。
*
* @param string $table ログテーブル名。
* @param int $retention_days ログの保持日数。
* @return int 削除された行数。
*/
function purge_old_logs( string $table, int $retention_days = 30 ): int {
global $wpdb;
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) );
return (int) $wpdb->query(
$wpdb->prepare(
"DELETE FROM {$table} WHERE created_at < %s",
$cutoff
)
);
}
// WordPress cronにフックして自動クリーンアップ
add_action( 'wp_scheduled_delete', function () use ( $log_table ) {
purge_old_logs( $log_table, 30 );
} );
30日間は合理的なデフォルトです。トレンドを把握しインシデントを調査するのに十分な履歴データがあり、データのフットプリントを最小限に保てます。コンプライアンス要件に応じて調整してください――一部の業界規制はより長い保持を義務付けていますが、ほとんどのWordPressサイトでは30日間で十分以上です。
完全なスキーマ
参考として、プライバシーに配慮したセキュリティログの最小限のテーブルスキーマを示します:
CREATE TABLE wp_security_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
source_hash CHAR(64) NOT NULL,
event_type VARCHAR(50) NOT NULL,
metadata TEXT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_source_hash (source_hash),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
このスキーマに含まれていないものに注目してください:ip_addressカラムなし、user_agentカラムなし、referrerカラムなし。metadataフィールドには非識別的なイベント詳細(例:対象となったフォーム、検出されたスパムの種類)を保存できますが、生の個人情報は決して含めるべきではありません。
実際の運用における意味
このアプローチにより、セキュリティログには以下が含まれます:
- 24時間のウィンドウ内で同一ソースからのイベントを関連付けることができる仮名化された識別子(ハッシュ値)。
- 何が起きたかを示すイベントタイプ(スパム送信、バリデーション失敗、レート制限ヒット)。
- 時系列分析のためのタイムスタンプ。
- フォレンジックのコンテキストのためのオプションのメタデータ(フォームID、検出方法など)。
ログに含まれないもの:
- 生のIPアドレス。
- ユーザーエージェント文字列(フィンガープリンティングのベクターになり得る)。
- 自然人を直接的または間接的に識別できるデータ。
この区別は重要です。GDPR第11条の下では、記録からデータ主体を識別できない場合、いくつかのデータ主体の権利(アクセス、訂正、消去)は適用されません。ログインフラストラクチャの運用と防御が格段にシンプルになります。
WordPressスパム対策プラグインについて
ほとんどのWordPressスパム対策ソリューションは、デフォルトで生のIPアドレスを保存します。データベースに平文で保存するものもあれば、ローテーションやパージが行われないログファイルに書き込むものもあります。
Contact Form 7を使用しており、個人情報保護を真剣に考えたプラグインを探しているなら、Samurai Honeypot for Formsは内部のスパム検出にハッシュ化された匿名化識別子を使用しています。生のIPアドレスの保存なし、第三者へのデータ共有なし、GDPR上の責任なし。この記事で説明したのと同じハッシュ化アプローチを適用して、個人データを保持することなく、リピートオフェンダーの検出とレート制限の調整を行います。
上記のコードスニペットはフレームワークに依存しません。任意のPHPアプリケーション、任意のWordPressプラグイン、任意のカスタムログシステムに適用できます。原則はどこでも同じです:保存前にハッシュ化し、ソルトをローテーションし、スケジュールに従ってパージする。
まとめ
- GDPRの下では、IPアドレスは個人データです。 セキュリティログに生のまま保存することは、意図にかかわらずコンプライアンスリスクを生じさせます。
- ローテーションソルトを用いたハッシュ化は、リバーシブルな個人情報を保存することなく、パターン検出のための決定論的な識別子を提供します。ベースラインのアプローチとして
sha256(ip + hmac_derived_daily_salt)を使用してください。 - 切り詰めではなく、ハッシュ化を。 トランケーションは、GDPRの下で個人データに該当する可能性のある部分的な識別子を残し、セキュリティのユースケースにおいて衝突耐性が弱くなります。
- ソルトは日次でローテーションしてください(または選択した間隔で)。再識別リスクを制限するためです。静的ソルトは単一障害点です。
- 古いログは自動的にパージしてください。 匿名化されたデータであっても、無期限に保持すべきではありません。30日間で、ほとんどのインシデント対応シナリオをカバーできます。
- 制約を前提にスキーマを設計してください。 データベーステーブルに
ip_addressカラムがあるなら、すでに誤った判断を下しています。source_hashから始めて、そこから構築してください。