PHPセキュリティ対策:PHPでreCAPTCHAを使う

(1/1)
reCAPTCHAロゴ
reCAPTCHA は、ボットがサイトの入力画面からデータ入力するのを防ぐための技術で、2007年(平成19年)に米カーネギー大学が開発し、2009年(平成21年)にGoogleが買い取った。
みなさんも、サイトの入力画面に左図のロゴが表示されるのをご覧になったことがあるだろう。当サイトでも、「メッセージ受付」画面で reCAPTCHA を利用している。
今回は、PHPを使い、このメッセージ受付画面と同じものを作ってみることにする。

サンプル・プログラムの実行例

PHPでreCAPTCHAを使う
reCAPTCHA を利用する入力画面は、右下にロゴが表示される。
reCAPTCHAは、この画面で「送信」ボタンをクリックするのがボットではなく、人間であることを判定する。

目次

サンプル・プログラムのダウンロード

圧縮ファイルの内容
message.phpサンプル・プログラム
.pahooEnvクラウドサービスを利用するためのアカウント情報などを記入する .env ファイル。
使い方は「各種クラウド連携サービス(WebAPI)の登録方法」を参照。include_path が通ったディレクトリに配置すること。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
message.php 更新履歴
バージョン 更新日 内容
2.1.0 2025/09/15 .pahooEnv導入
2.0 2020/05/29 reCAPTCHA v3導入
1.1 2009/10/28 受付番号を付加
1.0 2009/08/25 初版
pahooInputData.php 更新履歴
バージョン 更新日 内容
2.0.1 2025/08/11 getParam() bug-fix
2.0.0 2025/08/11 pahooLoadEnv() 追加
1.9.0 2025/07/26 getParam() 引数に$trim追加
1.8.1 2025/03/15 validRegexPattern() debug
1.8.0 2024/11/12 validRegexPattern() 追加

準備:PHP の https対応

クラウド連携や相手先サイトのデータを読み込むのに https通信を使うため、PHPに OpenSSLモジュールが組み込まれている必要がある。関数  phpinfo  を使って、下図のように表示されればOKだ。
OpenSSL - PHP
そうでない場合は、次の手順に従ってOpenSSLを有効化し、PHPを再起動させる必要がある。

Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dll
Linuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順

これで準備は完了だ。

準備:pahooInputData 関数群

PHPのバージョンや入力データのバリデーションなど、汎用的に使う関数群を収めたファイル "pahooInputData.php" が同梱されているが、include_path が通ったディレクトリに配置してほしい。他のプログラムでも "pahooInputData.php" を利用するが、常に最新のファイルを1つ配置すればよい。

また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。

準備:reCAPTCHAの利用登録

reCAPTCHAの利用登録
まず、Google reCAPTCHA から利用登録を行う。

reCAPTCHA にはV2とV3があるが、今回は利用者に負担をかけることが少ないV3を使用することにする。
また、Googleアカウントがない方は、事前にアカウントを入手してほしい。

Google reCAPTCHAの右上にある「Admin console」をクリックする。初回は左図のような画面が表示されるので、必要事項を選択し、reCAPTCHA を使用するドメインを入力し、「送信」を押下する。
reCAPTCHAの利用登録
登録が完了すると、左図の画面に移動する。
サイトキーシークレットキー は、これから作るプログラムで利用するので、メモしておく。

message.php

  55: // 各種定数(START) ===========================================================
  56: 
  57: // チェックするURLパターン
  58: define('PAT_URL', "/^https?\:\/\/[a-z\.]*pahoo\.org/ui");
  59: 
  60: // 送信先メールアドレス
  61: define('MAIL_TO', 'aaaa@bbbb.ccc');
  62: 
  63: // Google reCAPTCHA v3
  64: // https://www.google.com/recaptcha/intro/v3.html から無償入手
  65: // サイトキー
  66: if (isset($_ENV['PAHOO_RECAPTCHA_SITE_KEY'])) {
  67:     define('SITE_KEY', $_ENV['PAHOO_RECAPTCHA_SITE_KEY']);
  68: else {
  69:     define('SITE_KEY', '');
  70: }
  71: // シークレットキー
  72: if (isset($_ENV['PAHOO_RECAPTCHA_SECRET_KEY'])) {
  73:     define('SECRET_KEY', $_ENV['PAHOO_RECAPTCHA_SECRET_KEY']);
  74: else {
  75:     define('SECRET_KEY', '');
  76: }
  77: 
  78: // トークンネーム(固定)
  79: define('TOKEN_NAME', 'recaptchaToken');
  80: 
  81: // 表示幅(ピクセル)
  82: define('WIDTH', 600);
  83: 
  84: // 各種定数(END) ===============================================================

入手したサイトキーシークレットキー は、.pahooEnv ファイル、もしきは本プログラムの、それぞれの定数に格納しておく。

今回のプログラムでは、問い合わせ内容をサイト管理者のメールアドレスへ送信する。送信先メールアドレスは定数 MAIL_TO に、問い合わせURLが自サイトかどうかを判別するための正規表現パターンを定数 PAT_URL に、それぞれ定義しておく。

reCAPTCHAの仕組み

reCAPTCHAの仕組み
reCAPTCHA ウィジェットはGoogleからJavaScriptの形で提供されている。これを組み込んだフォームから、サイトキーを使って Google reCAPTCHA サーバにトークンを要求する。
取得したトークンと、その他入力データをセットにしてスコア判定プログラム(サーバサイド)に送る。判定プログラムは入手したトークンと、シークレットキーを使い、Google reCAPTCHA サーバにトークン検査を要求する。すると、そのトークンに対応するスコアが返される。スコアの価は0.0以上1.0以下で、おおむね0.5未満はbotの可能性がある。

Google reCAPTCHA サーバがどのような方法でスコア計算しているかは明らかにされていないが、当サイトでは、この reCAPTCHA を導入することで、迷惑メッセージが皆無になったことから、結果として効果を発揮している。

プログラムの流れ

PHPでreCAPTCHAを使う
プログラムは、メッセージ入力から reCAPTCHA によるスコア判定、送信完了画面まで、すべて1本のPHPプログラムを使って行い、パラメータによって表示する画面を切り替える。

入力チェック画面

message.php

 276: /**
 277:  * HTML BODYを作成する - 入力チェック用
 278:  * @param   string $name    ニックネーム
 279:  * @param   string $url     関連URL
 280:  * @param   string $message メッセージ
 281:  * @param   bool   $private 非公開フラグ
 282:  * @return  string HTML BODY
 283: */
 284: function makeBodyCheck($name, $url, $message, $private) {
 285:     $myself = MYSELF;
 286: 
 287:     // 入力チェック
 288:     $flag = TRUE;
 289:     if ($name == '') {
 290:         $flag = FALSE;
 291:         $name_msg = '<span class="red">※ニックネームは必須です.入力してください.</span>';
 292:     } else {
 293:         $name_msg = '';
 294:     }
 295: 
 296:     
 297:     if (($title = checkURL($url)) == FALSE) {
 298:         $flag = FALSE;
 299:         $url_msg = '<span class="red">※当サイトのURLではありません.再入力してください.</span>';
 300:     } else {
 301:         $url_msg = '';
 302:     }
 303: 
 304:     if ($message == '') {
 305:         $flag = FALSE;
 306:         $message_msg = '<span class="red">※メッセージは必須です.入力してください.</span>';
 307:     } else {
 308:         $message_msg = '';
 309:     }
 310: 
 311:     // 非公開フラグの処理
 312:     $private_msg = $private ? 'メッセージは非公開にする.' : 'メッセージは公開する.';
 313:     $private = $private ? '<input type="hidden" name="private" value="TRUE">' : '';
 314: 
 315:     // ボタン作成
 316:     $button = $flag ? '<input type="submit" name="send" value="送信">' : '';
 317: 
 318: // reCAPTCHA v3対応フォーム
 319: $site_key   = SITE_KEY;
 320: $token_name = TOKEN_NAME;
 321: $body =<<< EOT
 322: <script src="https://www.google.com/recaptcha/api.js?render={$site_key}" async defer></script>
 323: <script>
 324: grecaptcha.ready(function() {
 325:     grecaptcha.execute('{$site_key}',
 326:         {action:'contact'}).then(function(token) {
 327:         var token_name = document.getElementById('{$token_name}');
 328:             token_name.value = token;
 329:     });
 330: });
 331: </script> 
 332: <!-- タイトル -->
 333: <h1 class="index1">メッセージ内容確認</h1>
 334: 
 335: <!-- 本文 -->
 336: <div class="main">
 337: <p>
 338: ご意見、ご質問、励ましのメッセージを当サイト管理者へ送信します。<br>
 339: 下記の内容で問題なければ、「送信」ボタンをクリックしてください。
 340: </p>
 341: 
 342: <p>
 343: ●ニックネーム {$name_msg}
 344: <blockquote class="quote2">{$name}</blockquote>
 345: </p>
 346: <p>
 347: ●関連URL {$url_msg}<span class="blue">{$title}</span>
 348: <blockquote class="quote2">{$url}</blockquote>
 349: </p>
 350: <p>
 351: ●メッセージ {$message_msg}
 352: <blockquote class="quote2">{$message}</blockquote>
 353: ●{$private_msg}
 354: </p>
 355: <form name="form_message" id="form_message" method="post" action="{$myself}">
 356: <div>
 357:  <input type="submit" name="input" id="input" value="入力画面に戻る"> 
 358: {$button}
 359: </div>
 360: <input type="hidden" name="name" id="name" value="{$name}">
 361: <input type="hidden" name="url"  id="url"  value="{$url}">
 362: <input type="hidden" name="message" id="message" value="{$message}">
 363: <input type="hidden" name="{$token_name}" id="{$token_name}">
 364: {$private}
 365: </form>
 366: 
 367: <p>
 368: <a href="/">トップページへ戻る</a>
 369: </p>
 370: </div>
 371: </body>
 372: 
 373: EOT;
 374:     return $body;
 375: }

データ入力画面を生成するユーザー関数 makeBodyInput からのデータを受け取り、データ内容を検査するとともに、reCAPTCHA にトークン要求をするユーザー関数が makeBodyChec である。
前半で、入力データの検査を行う。

中盤(表示HTMLのbodyタグの冒頭)に、reCAPTCHA にトークン要求をするJavaScriptを記述する。
取得したトークンは定数 TOKEN_NAME で定義されたオブジェクトに格納され、次の画面に渡される。

reCAPTCHA v3 APIからデータ取得

message.php

 194: /**
 195:  * reCAPTCHA v3 APIからデータ取得
 196:  * @param   string $token トークン
 197:  * @return  array  応答データ
 198: */
 199: function get_reCAPTCHA($token) {
 200:     $secret_key = SECRET_KEY;
 201: 
 202:     $ch = curl_init();
 203:     curl_setopt($ch, CURLOPT_URL, 'https://www.google.com/recaptcha/api/siteverify');
 204:     curl_setopt($ch, CURLOPT_POST, TRUE);
 205: 
 206:     // API パラメータの指定
 207:     curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
 208:         'secret' => $secret_key,
 209:         'response' => $token
 210:     )));
 211:     curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
 212: 
 213:     $api_response = curl_exec($ch);
 214:     curl_close($ch);
 215: 
 216:     return json_decode($api_response);
 217: }

先ほど取得したトークンとシークレットキーを使って reCAPTCHA に問い合わせを行うユーザー関数が get_reCAPTCHA である。

Google reCAPTCHA は、入力パラメータ(IN)は POST 渡しで、出力結果(OUT)は JSON で戻るというWebAPIである。
WebAPIのURL
URL
https://www.google.com/recaptcha/api/siteverify

入力パラメータ
フィールド名 要否 内  容
secret 必須 シークレットキー
response 必須 トークン
応答データ(json) success トークンの真偽(TRUE|FALSE) score スコア(0.0以上1.0以下) action アクション名 challenge_ts タイムスタンプ hostname ホスト名 error-codes エラーメッセージ

送信完了

message.php

 479: // 処理分岐
 480: if (isset($_POST['check'])) {
 481:     $HtmlBody = makeBodyCheck($name, $url, $message, $private);
 482: else if (isset($_POST['send'])) {
 483:     $res = get_reCAPTCHA($token);
 484:     if (isset($res->success&& $res->success && isset($res->score&& ($res->score >0.5)) {
 485:         $HtmlBody = makeBodySend($name, $url, $message, $private);
 486:     } else {
 487:         $HtmlBody = makeBodyFail($name, $url, $message, $private);
 488:     }
 489: else {
 490:     $HtmlBody = makeBodyInput($name, $url, $message, $private);
 491: }
 492: 
 493: // 表示処理
 494: echo $HtmlHeader;
 495: echo $HtmlBody;
 496: echo $HtmlFooter;

前述のユーザー関数 get_reCAPTCHA で取得したスコアが0.5以上であれば、ユーザー関数 makeBodySend を実行し、メッセージのメール送信と送信完了画面を表示する。
そうでなければ、ユーザー関数 makeBodyFail を実行し、メール送信せずに、送信失敗したことをユーザーに通知する。
前半で、入力データの検査を行う。

偽reCAPTCHA

reCAPTCHAを偽装し、マルウェアを感染させようとする手口が横行しているとして、マイクロソフトなどが注意を呼びかけている。
実際、2024年(令和6年)12月に旅行予約サイト「Booking.com」を装うフィッシング詐欺に利用され、この攻撃は2025年(令和7年)2月時点でも続いている。ユーザーが画面の指示に従うとパスワードなどの情報を盗み出すマルウェアに感染し、アカウントを乗っ取られたり決済情報を盗まれたりする恐れがある。

ClickFix と呼ばれる手口では、普段利用しているサービスのログイン画面に見せかけた詐欺サイトにユーザーを誘導し、「私はロボットではありません」を装う偽の reCAPTCHA ボタンを表示する。ユーザーがこのボタンをクリックすると、確認のためと称して次のようなキーボード操作を求められる。
  1. Windowsボタンと「R」を押す
  2. 「CTRL」と「V」を押す
  3. 「Enter」を押す
1.で「ファイル名をして実行する」ウィンドウを開き、2.で不正サイトでクリップボードにコピーされた不正コードをペーストする。そして、3.で不正コードを実行させる。原始的な方法ではあるが、それゆえ、騙されれば確実にマルウェアに感染してしまう。

利用者側としては、 reCAPTCHA はブラウザ以外の操作を求めることはないことを認識しておきたい。
提供者側としては、自サイトが則られたり改ざんされることがないよう、定期的にサイト点検しておきたい。

生成AIによる突破

2025年(令和7年)6月現在、OpenAIのブラウザを介して複雑なタスクを自動で処理できるAIエージェント Operator などの生成AIreCAPTCHA v3 を突破できることがわかっている。reCAPTCHA v3 は実際のブラウザ環境を利用するAIエージェントを検出することができないためだ。

このようなAIエージェントを正確に検出するために、人間がコンピューターと物理的にやり取りする際の独特の行動パターンや認知特性に焦点を当てた新しいアプローチに注目が集まっている。
たとえば Proof-of-Human API は、このような行動パターンや認知特性をベースに人間とAIを区別することができるという。

参考サイト

(この項おわり)
header