recaptcha3.inc.php

サマリGoogle reCAPTCHA v3 によるスパム対策
リビジョン1.0
対応バージョン1.5.3
投稿者M.Taniguchi
投稿日2020-04-25 (土) 02:21:45

概要

Google reCAPTCHA v3 によるスパム対策プラグイン。

ページ編集・コメント投稿・ファイル添付など、PukiWiki標準の編集機能をスパムから守ります。
reCAPTCHA v3 は不審な送信者を学習により自動判定する不可視の防壁です。煩わしい文字入力をユーザーに要求せず、ウィキのユーザビリティーに影響しません。

追加ファイルはこのプラグインコードだけ。PukiWiki本体の変更も最小限にし、なるべく簡単に導入できるようにしています。
が、そのための副作用として、JavaScriptを活用する高度な編集系サードパーティ製プラグインとは相性が悪いかもしれません。
PukiWikiをほぼ素のままで運用し、手軽にスパム対策したいかた向けです。

PukiWiki 1.5.3/PHP 7.4/UTF-8/主要モダンブラウザーで動作確認済み。旧バージョンでも動くかもしれませんが非推奨です。

導入手順

以下の手順に沿ってPukiWikiに導入してください。

  1. 本プラグイン recaptcha3.inc.php を plugin ディレクトリに設置する。
  2. Google reCAPTCHA サイト(https: //www.google.com/recaptcha/)でウィキのドメインを「reCAPTCHA v3」タイプで登録し、取得したサイトキー・シークレットキーを本プラグイン内の定数 PLUGIN_RECAPTCHA3_SITE_KEY, PLUGIN_RECAPTCHA3_SECRET_KEY に設定する。
  3. スキンファイル skin/pukiwiki.skin.php のほぼ末尾、「</body>」(275行目あたり)の直前に次のコードを挿入する。
    <?php if (exist_plugin_convert('recaptcha3')) echo do_plugin_convert('recaptcha3'); // reCAPTCHA v3 plugin ?>
  4. ライブラリファイル lib/plugin.php の「function do_plugin_action($name)」関数内、「$retvar = call_user_func('plugin_' . $name . '_action');」行の直前(92行目あたり)に次のコードを挿入する。
    if (exist_plugin_action('recaptcha3') && !call_user_func_array('plugin_recaptcha3_action', array($name))['body']) die_message('Rejected by Google reCAPTCHA v3'); // reCAPTCHA v3 plugin

コード

recaptcha3.inc.php
(下記のコードをコピーして、plugin ディレクトリに recaptcha3.inc.php というファイル名で保存してください)

<?php
/**
PukiWiki - Yet another WikiWikiWeb clone.
recaptcha3.inc.php, v1.0 2020 M.Taniguchi
License: GPL v3 or (at your option) any later version

Google reCAPTCHA v3 によるスパム対策プラグイン。

ページ編集・コメント投稿・ファイル添付など、PukiWiki標準の編集機能をスパムから守ります。
reCAPTCHA v3 は不審な送信者を学習により自動判定する不可視の防壁です。煩わしい文字入力をユーザーに要求せず、ウィキのユーザビリティーに影響しません。

追加ファイルはこのプラグインだけ。PukiWiki本体の変更も最小限にし、なるべく簡単に導入できるようにしています。
が、そのための副作用として、JavaScriptを活用する高度な編集系サードパーティ製プラグインとは相性が悪いかもしれません。
PukiWikiをほぼ素のままで運用し、手軽にスパム対策したいかた向けです。

【導入手順】
以下の手順に沿ってPukiWikiに導入してください。

1) Google reCAPTCHA サイトでウィキのドメインを「reCAPTCHA v3」タイプで登録し、取得したサイトキー・シークレットキーをこのプラグインの定数 PLUGIN_RECAPTCHA3_SITE_KEY, PLUGIN_RECAPTCHA3_SECRET_KEY に設定する。

2) スキンファイル skin/pukiwiki.skin.php のほぼ末尾、「</body>」(275行目あたり)の直前に次のコードを挿入する。
   <?php if (exist_plugin_convert('recaptcha3')) echo do_plugin_convert('recaptcha3'); // reCAPTCHA v3 plugin ?>

3) ライブラリファイル lib/plugin.php の「function do_plugin_action($name)」関数内、「$retvar = call_user_func('plugin_' . $name . '_action');」行の直前(92行目あたり)に次のコードを挿入する。
   if (exist_plugin_action('recaptcha3') && !call_user_func_array('plugin_recaptcha3_action', array($name))['body']) die_message('Rejected by Google reCAPTCHA v3'); // reCAPTCHA v3 plugin

【ご注意】
・PukiWiki 1.5.3/PHP 7.4/UTF-8/主要モダンブラウザーで動作確認済み。旧バージョンでも動くかもしれませんが非推奨です。
・標準プラグイン以外の動作確認はしていません。サードパーティ製プラグインによっては機能が妨げられる場合があります。
・JavaScriptが有効でないと動作しません。
・サーバーからreCAPTCHA APIへのアクセスにcURLを使用します。
・reCAPTCHA v3 について詳しくはGoogleのreCAPTCHAサイトをご覧ください。https: //www.google.com/recaptcha/
*/

define('PLUGIN_RECAPTCHA3_SITE_KEY',   '取得したサイトキーをここに書く');	// reCAPTCHA v3 サイトキー
define('PLUGIN_RECAPTCHA3_SECRET_KEY', '取得したシークレットキーをここに書く');	// reCAPTCHA v3 シークレットキー

define('PLUGIN_RECAPTCHA3_SCORE_THRESHOLD', 0.5);	// スコア閾値(0.0~1.0)。reCAPTCHAによる判定スコアがこの値より低い送信者はスパマーとみなして要求を拒否する。なお、直接プラグインURLを叩く種類のロボットはスコアによらず必ず拒否される

define('PLUGIN_RECAPTCHA3_DISABLED', 0);	// 本プラグイン機能を無効にする。メンテナンス用
define('PLUGIN_RECAPTCHA3_HIDE_BADGE', 1);	// reCAPTCHAバッジを非表示にし、代替文言を出力する。Googleの規約によりバッジか文言どちらかの表示が必須
define('PLUGIN_RECAPTCHA3_API_TIMEOUT', 0);	// reCAPTCHA APIタイムアウト時間(秒)。0なら無指定


// プラグイン出力
function plugin_recaptcha3_convert() {
	// 本プラグインまたはJavaScriptが無効なら何もしない
	if (PLUGIN_RECAPTCHA3_DISABLED || PKWK_READONLY || !PKWK_ALLOW_JAVASCRIPT) return '';

	// 二重起動禁止
	static	$included = false;
	if ($included) return '';
	$included = true;

	// reCAPTCHAバッジ非表示なら代替文言設定
	$protocol = 'https:';
	$badge = (!PLUGIN_RECAPTCHA3_HIDE_BADGE)? '' : '<style>.grecaptcha-badge{visibility:hidden} #_p_recaptcha3_terms{font-size:70%}</style><div id="_p_recaptcha3_terms">This site is protected by reCAPTCHA and the Google <a href="' . $protocol . '//policies.google.com/privacy" rel="noopener nofollow external">Privacy Policy</a> and <a href="' . $protocol . '//policies.google.com/terms" rel="noopener nofollow external">Terms of Service</a> apply.</div>';

	// JavaScript
	$siteKey = PLUGIN_RECAPTCHA3_SITE_KEY;
	$apiUrl = $protocol . '//www.google.com/recaptcha/api.js?render=';
	$js = <<<EOT
<script src="${apiUrl}${siteKey}" defer></script>
<script>
window.addEventListener('load', function(){
	new __PluginRecaptcha3__();
});

__PluginRecaptcha3__ = function() {
	const	self = this;
	this.timer = null;

	// 設定
	this.update();

	// DOMを監視し、もしページ内容が動的に変更されたら再設定する(モダンブラウザーのみ対応)
	const observer = new MutationObserver(function(mutations){ mutations.forEach(function(mutation){ if (mutation.type == 'childList') self.update(); }); });
	if (observer) {
		const target = document.getElementsByTagName('body')[0];
		if (target) observer.observe(target, { childList: true, subtree: true });
	}
};

// 設定
__PluginRecaptcha3__.prototype.setup = function() {
	const	self = this;

	// 全form要素を走査
	var	elements = document.getElementsByTagName('form');
	for (var i = elements.length - 1; i >= 0; --i) {
		var	form = elements[i];

		// こちらのタイミングで送信するため、既定の送信イベントを止めておく
		form.addEventListener('submit', self.stopSubmit, false);

		// form内全submitボタンを走査しクリックイベントを設定
		var eles = form.querySelectorAll('input[type="submit"]');
		for (var j = eles.length - 1; j >= 0; --j) eles[j].addEventListener('click', self.submit, false);
	}
};

// 再設定
__PluginRecaptcha3__.prototype.update = function() {
	const	self = this;
	if (this.timer) clearTimeout(this.timer);
	this.timer = setTimeout(function() { self.setup(); self.timer = null; }, 50);
};

// 送信防止
__PluginRecaptcha3__.prototype.stopSubmit = function(e) {
	e.preventDefault();
	e.stopPropagation();
	return false;
};

// クリック時送信処理
__PluginRecaptcha3__.prototype.submit = function(e) {
	var	form;
	if (this.closest) {
		form = this.closest('form');
	} else {
		for (form = this.parentNode; form; form = form.parentNode) if (form.nodeName.toLowerCase() == 'form') break;	// 旧ブラウザー対策
	}

	// クリックされたsubmitボタンのname,value属性をhiddenにコピー(submitボタンが複数ある場合への対処)
	if (form)  {
		var nameEle = form.querySelector('.__plugin_recaptcha3_submit__');
		var	name = this.getAttribute('name');
		if (name) {
			var	value = this.getAttribute('value');
			if (!nameEle) {
				form.insertAdjacentHTML('beforeend', '<input type="hidden" class="__plugin_recaptcha3_submit__" name="' + name + '" value="' + value + '"/>');
			} else {
				nameEle.setAttribute('name', name);
				nameEle.setAttribute('value', value);
			}
		} else
		if (nameEle) {
			if (nameEle.remove) nameEle.remove();
			else nameEle.parentNode.removeChild(nameEle);
		}

		// reCAPTCHAトークン取得
		grecaptcha.ready(function() {
			try {
				grecaptcha.execute('${siteKey}').then(function(token) {
					// 送信パラメーターにトークンを追加
					var ele = form.querySelector('input[name="__plugin_recaptcha3__"]');
					if (!ele) {
						form.insertAdjacentHTML('beforeend', '<input type="hidden" name="__plugin_recaptcha3__" value="' + token + '"/>');
					} else {
						ele.setAttribute('value', token);
					}
					// フォーム送信
					form.submit();
				});
			} catch(e) {}
		});
	}
	return false;
};
</script>
EOT;

	return $badge . $js;
}


// 受信リクエスト確認
function plugin_recaptcha3_action() {
	$result = true;	// 送信者判定結果(許可:true, 拒否:false)

	// 機能有効かつPOSTメソッド?
	if (!PLUGIN_RECAPTCHA3_DISABLED && !PKWK_READONLY && $_SERVER['REQUEST_METHOD'] == 'POST') {
		/* 【対象プラグイン設定テーブル】
		   reCAPTCHA判定の対象とするプラグインを列挙する配列。
		   パターン1 … array('name' => プラグイン名)
		   パターン2 … array('name' => プラグイン名, 'vars' => 必須クエリーパラメーター名) */
		$targetPlugins = array(
			array('name' => 'article'),
			array('name' => 'attach'),
			array('name' => 'bugtrack'),
			array('name' => 'comment'),
			array('name' => 'edit', 'vars' => 'write'),	// editプラグインはページ更新(writeパラメーターあり)時のみ対象とする
			array('name' => 'insert'),
			array('name' => 'loginform'),
			array('name' => 'memo'),
			array('name' => 'pcomment'),
			array('name' => 'rename'),
			array('name' => 'tracker'),
			array('name' => 'vote'),
		);

		global	$vars;
		list($name) = func_get_args();
		foreach ($targetPlugins as $target) {
			if ($target['name'] != $name) continue;	// プラグイン名一致?
			if (!isset($target['vars']) || isset($vars[$target['vars']])) {	// クエリーパラメーター未指定、または指定名が含まれる?
				if (!isset($vars['__plugin_recaptcha3__']) || $vars['__plugin_recaptcha3__'] == '') {	// reCAPTCHAトークンあり?
					// トークンのない不正要求なら送信者を拒否
					$result = false;
				} else {
					// reCAPTCHA API呼び出し
					$ch = curl_init('https:'.'//www.google.com/recaptcha/api/siteverify');
					curl_setopt($ch, CURLOPT_POST, true);
					curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array('secret' => PLUGIN_RECAPTCHA3_SECRET_KEY, 'response' => $vars['__plugin_recaptcha3__'])));
					curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
					curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
					if (PLUGIN_RECAPTCHA3_API_TIMEOUT > 0) curl_setopt($ch, CURLOPT_TIMEOUT, PLUGIN_RECAPTCHA3_API_TIMEOUT);
					$data = json_decode(curl_exec($ch));
					curl_close($ch);

					// スコアが閾値未満なら送信者を拒否
					if (!$data->success || $data->score < PLUGIN_RECAPTCHA3_SCORE_THRESHOLD) $result = false;
				}
				break;
			}
		}
	}

	return array('msg' => 'recaptcha3', 'body' => $result);
}

ご注意

詳細

動作設定

コード内の下記の定数で動作を制御することができます。

定数名既定値意味
PLUGIN_RECAPTCHA3_SITE_KEY文字列reCAPTCHA v3 サイトキー。取得したキーを必ず設定すること
PLUGIN_RECAPTCHA3_SECRET_KEY文字列reCAPTCHA v3 シークレットキー。取得したキーを必ず設定すること
PLUGIN_RECAPTCHA3_SCORE_THRESHOLD0.0~1.00.5スコア閾値(0.0~1.0)。reCAPTCHAによる判定スコアがこの値より低い送信者は拒否される
PLUGIN_RECAPTCHA3_DISABLED0 or 10本プラグイン機能を無効にする。メンテナンス用
PLUGIN_RECAPTCHA3_HIDE_BADGE0 or 11reCAPTCHAバッジを非表示にし、代替文言を出力する。Googleの規約によりバッジか文言どちらかの表示が必須
PLUGIN_RECAPTCHA3_API_TIMEOUT任意の数値0reCAPTCHA APIタイムアウト時間(秒)。0なら無指定

スパム拒否の仕組み

  1. ブラウザー側において、JavaScriptによってページ内のすべてのform要素を探し出し、submitボタンがクリックされたらreCAPTCHAトークンを取得して送信パラメーターに含めるよう細工する。
    → 副作用として、この細工がサードパーティ製プラグインの動作を妨げる可能性がある
  2. サーバー側において、受信したリクエストがPOSTメソッドかつ既知のプラグイン呼び出しなら次の判定を行う。
    1. パラメーターにreCAPTCHAトークンが含まれなければ、不正アクセスとみなしてリクエストを拒否する。
      → フォームを介さず直接プラグインURLを叩く種類のロボットはすべて弾かれる
    2. reCAPTCHA APIにトークンを送信し、応答スコアが閾値未満ならスパマーとみなしてリクエストを拒否する。
      → 機械的なフォーム操作や不審な送信元IPアドレスなどはスコアが低く弾かれる
      → 学習も絡みスコア基準は曖昧だが、もし効果が薄い・または効き過ぎるといった問題があれば PLUGIN_RECAPTCHA3_SCORE_THRESHOLD 定数値で調整できる
      → 手入力による散発的ないたずら書き込みの類いは、正当な編集と区別できず弾くことができない(矯激・卑俗な書き込みを極端に繰り返せば学習されるかもしれないが)

高度な設定:対象プラグインの追加

本プラグインはデフォルトで、PukiWikiに標準添付の編集系プラグインのみをスパム判定の対象としています。具体的には次の通り。

article, attach, bugtrack, comment, edit, insert, loginform, memo, pcomment, rename, tracker, vote

スパムボットは標準プラグインを標的にすると考えられるため、一般的にはこれで十分なはずです。
しかし、もし特定のサードパーティ製プラグインを標的として攻撃されていたら、コード内の $targetPlugins 配列にそのプラグイン名を他行に倣って追加してください。
ただし上で述べた通り、プラグインの編集・投稿機能がPOSTメソッドのform要素かつsubmitボタンで送信する仕組みになっていないと効果がなく、処理内容による相性にも左右されます。

動作確認

本プラグインが正しく導入されていれば、ページ末尾に「This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.」との文言(定数設定によってはreCAPTCHAバッジ)が表示されます。
この状態でページ編集やコメント投稿ができていればOKです。

逆に拒否される場合を確認したければ、シークレットキーの値をわざと不正にしてみてください。
その状態でページ編集などを試みると、「Rejected by Google reCAPTCHA v3」とエラーメッセージが表示されるはずです。
スパムに対する正しいテストケースではありませんが、少なくともプラグインが動作しreCAPTCHA APIと連絡していることは確かめられます。
実際のスパム攻撃については、Google reCAPTCHAサイトの管理画面に統計が表示されます。スコア閾値調整の参考にもなるでしょう。

なお、本プラグインが正しく導入されていても、古いブラウザーでは常に編集に失敗するかもしれませんが、仕様としてご了承ください。
編集ができないだけで、閲覧には支障ないと思います。

閲覧専用(PKWK_READONLY が 1)のウィキにおいては、本プラグインは何もしません。

ライセンス

GPL v3


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2020-04-25 (土) 08:26:25
Site admin: PukiWiki Development Team

PukiWiki 1.5.3+ © 2001-2020 PukiWiki Development Team. Powered by PHP 5.6.40-0+deb8u11. HTML convert time: 0.216 sec.

OSDN