WordPress

最終更新日:
公開日:

ワークショップ

オリジナルのWordPressプラグインを作る(8)

WordPressにお問い合わせページを作成する(6)

お問い合わせページにセッションを使ってブラウザ再読み込み対策とCSRF対策を実装します。

この記事のポイント

  • セッショントークンを使ってページの移動が正しく行われているか確認する
  • セッションの有無でデータ登録の重複を防ぐ

目次

セッショントークンを使ってCSRF対策を行う

前回の「WordPressにお問い合わせページを作成する(5)」ではお問い合わせページに入力した内容をデータベースに保存する機能を実装してきました。

前回作成したコード:GitHub

現時点ではブラウザの再読み込みを行うと同じデータが複数登録されてしまう問題があります。
試しに、ブラウザでお問い合わせページを開き、入力ページから完了ページまで進み、以下の完了ページが表示されたらブラウザで再読み込み(リロード)してみてください。

完了ページで再読み込みを行うと多重でデータが登録される
完了ページで再読み込みを行うと多重でデータが登録される

再読み込みを行った数だけ、データベースに同じデータが重複して登録されることを確認できます。

同じデータが複数登録されてしまう
同じデータが複数登録されてしまう

この問題を解決するためにセッションを使ってページの移動を制御するようにしていきます。
ページの移動に不正がないかを確認することで、同時にCSRF対策も行います。

セッションを使用できる状態にする

まずはお問い合わせページでセッションを使用できるようにsession_start関数を実行して開始状態にしましょう。
page-contact.phpを開いて、以下のコードを追加してください。

page-contact.php

<?php

// フォームの入力できる最大文字数を設定
define( 'MAX_NAME_LIMIT', 10);
define( 'MAX_EMAIL_LIMIT', 100);
define( 'MAX_CONTENT_LIMIT', 100);

// 変数の初期化
$error = array();
$paeg_flag = 1;

if( session_status() !== PHP_SESSION_ACTIVE ) {
	session_start();
}

if( isset($_POST['btn_confirm']) && $_POST['btn_confirm'] !== null ) {

----- 省略 -----

session_start関数を実行する前に、if文ですでにセッションが開始状態になっているか確認します。
session_status関数と定数PHP_SESSION_ACTIVEの値が等しいときはすでにセッションが開始状態になっているためsession_start関数を重複して実行しないようにします。

session_status関数の戻り値はセッションが使用できない環境では0、使用できる環境で開始状態ではないときは1、使用できる環境ですでに開始状態になっているきは2になります。

続いて、お問合せページの入力ページを表示したタイミングでページのセッショントークンを作成するようにします。

page-contact.php

<?php

----- 省略 -----

	else: ?>

		<?php

		// セッション作成
		$token = bin2hex(openssl_random_pseudo_bytes(24));
		$_SESSION['token'] = $token;

		?>

		<?php if( !empty($error) ): ?>
		<ul class="list_error">
			<?php foreach( $error as $e ): ?>
				<li><?php echo $e; ?></li>
			<?php endforeach; ?>
		</ul>
		<?php endif; ?>

		<form action="" method="post">
			<dl>
				<dt>お名前</dt>
				<dd><input type="text" name="input_name" value="<?php if( !empty($_POST['input_name']) ) { echo htmlspecialchars($_POST['input_name'], ENT_QUOTES); } ?>"></dd>
				<dt>メールアドレス</dt>
				<dd><input type="email" name="input_email" value="<?php if( !empty($_POST['input_email']) ) { echo htmlspecialchars($_POST['input_email'], ENT_QUOTES); } ?>"></dd>
				<dt>お問い合わせ内容</dt>
				<dd>
					<textarea name="input_content"><?php if( !empty($_POST['input_content']) ) { echo htmlspecialchars($_POST['input_content'], ENT_QUOTES); } ?></textarea>
				</dd>
			</dl>
			<div class="btn_area">
				<input type="submit" name="btn_confirm" value="送信内容の確認">
			</div>
			<input type="hidden" name="csrf_token" value="<?php if( !empty($token) ){ echo htmlspecialchars( $token, ENT_COMPAT, 'UTF-8'); } ?>">
		</form>

	<?php endif; ?>
</article>

<?php get_footer(); ?>

追記したコードでは、openssl_random_pseudo_bytes関数bin2hex関数を組み合わせてランダムな文字列を生成しています。
openssl_random_pseudo_bytes関数で指定している24は生成する文字列の文字数です。
ここで生成した24文字のランダムな文字列をセッショントークンとして使用します。
セッショントークンは入力ページが表示される度に更新されるようになっています。

生成したセッショントークンはセッションのグローバル変数$_SESSIONに入れておき、入力ページにも非表示のinput要素にセットします。

input要素にセットしたセッショントークンは確認ページ、完了ページへと入力データとセットで$_POSTに入れる形で渡していき、その度に$_SESSION[‘token’]に入っているセッショントークンと等しいか確認します。
そうすることで、セッショントークンがある間はお問い合わせページの正常な操作を行っていることを証明することができる仕組みです。

最終的には完了ページを表示するタイミングで$_SESSION[‘token’]に入っているセッショントークンは削除します。
その結果、もし$_SESSION[‘token’]$_POSTの2つのセッショントークを比較して正しくない、または空のときは入力ページに戻るようにすることで、冒頭のブラウザ再読み込みによるデータの重複登録や不正な操作を予防することができます。

セッショントークンを確認するバリデーションを実装する

続いて、$_SESSION[‘token’]にセットしたセッショントークンと、入力ページのinput要素に入れたセッショントークンが等しいか確認するバリデーションを実装します。
バリデーションはvalidation関数にしているので、この中にコードを追加しましょう。

page-contact.php

<?php

----- 省略 -----

function validation( $data ) {

	$error = array();

	if( empty($_SESSION['token']) || $_SESSION['token'] !== $data['csrf_token'] ) {
		$error[] = 'フォームにご入力ください';
		return  $error;
	}

	if( empty($data['input_name']) ) {
		$error[] = 'お名前を入力してください';
	} else {

		if( MAX_NAME_LIMIT < mb_strlen($data['input_name']) ) {
			$error[] = '名前は'.MAX_NAME_LIMIT.'文字以内で入力してください。';
		}
	}

	if( empty($data['input_email']) ) {
		$error[] = 'メールアドレスを入力してください';
	} else {

		if( MAX_EMAIL_LIMIT < mb_strlen($data['input_email']) ) {
			$error[] = 'メールアドレスは'.MAX_EMAIL_LIMIT.'文字以内で入力してください。';
		}
	}

	if( empty($data['input_content']) ) {
		$error[] = 'お問い合わせ内容を入力してください';
	} else {

		if( MAX_CONTENT_LIMIT < mb_strlen($data['input_content']) ) {
			$error[] = 'お問い合わせ内容は'.MAX_CONTENT_LIMIT.'文字以内で入力してください。';
		}
	}

	return $error;
}

----- 省略 -----

もしセッショントークンが空、もしくは$_SESSION[‘token’]にセットしたセッショントークンと異なる場合はその時点でバリデーションを終了して入力ページに戻ります。

続いて、確認ページにも入力ページと同様に非表示のinput要素にセッショントークンをセットするコードを追加します。

page-contact.php

<?php

----- 省略 -----

<article id="contact">
	<header>
		<h1>お問い合わせ</h1>
	</header>

	<?php if( $page_flag === 2 ): ?>

		<p>送信内容をご確認いただき、よろしければ「送信」ボタンを押してください。</p>
		<form action="" method="post">
			<dl>
				<dt>お名前</dt>
				<dd><p><?php if( !empty($_POST['input_name']) ) { echo htmlspecialchars($_POST['input_name'], ENT_QUOTES); } ?></p></dd>
				<dt>メールアドレス</dt>
				<dd><p><?php if( !empty($_POST['input_email']) ) { echo htmlspecialchars($_POST['input_email'], ENT_QUOTES); } ?></p></dd>
				<dt>お問い合わせ内容</dt>
				<dd class="last"><p><?php if( !empty($_POST['input_content']) ) { echo nl2br(htmlspecialchars($_POST['input_content'], ENT_QUOTES)); } ?></p></dd>
			</dl>
			<div class="btn_area">
				<input type="submit" name="btn_back" value="修正する">
				<input type="submit" name="btn_submit" value="送信">
			</div>
			<input type="hidden" name="input_name" value="<?php if( !empty($_POST['input_name']) ) { echo htmlspecialchars($_POST['input_name'], ENT_QUOTES); } ?>">
			<input type="hidden" name="input_email" value="<?php if( !empty($_POST['input_email']) ) { echo htmlspecialchars($_POST['input_email'], ENT_QUOTES); } ?>">
			<input type="hidden" name="input_content" value="<?php if( !empty($_POST['input_content']) ) { echo htmlspecialchars($_POST['input_content'], ENT_QUOTES); } ?>">
			<input type="hidden" name="csrf_token" value="<?php if( !empty($_POST['csrf_token']) ){ echo htmlspecialchars($_POST['csrf_token'], ENT_QUOTES); } ?>">
		</form>

	<?php elseif( $page_flag === 3 ): ?>

		<p>お問い合わせいただきありがとうございます。<br>受付が完了いたしました。</p>
		<div class="link_area">
			<a class="btn_back" href="/">トップページに戻る</a>
		</div>
	
	<?php else: ?>

----- 省略 -----

セッショントークンを削除する

最後に、完了ページを表示するタイミングで移動するときに$_SESSION[‘token’]にセットしたセッショントークンを削除します。
具体的には、データベースにお問い合わせの入力内容が正常に保存できた後のタイミングでセッショントークンを削除するコードを追加しましょう。

page-contact.php

<?php

----- 省略 -----

} elseif( isset($_POST['btn_submit']) && $_POST['btn_submit'] !== null ) {

	$error = validation($_POST);

	if( empty($error) ) {

		global $wpdb;
		date_default_timezone_set('Asia/Tokyo');
		$current_datetime = date('Y-m-d H:i:s');

		$wpdb->insert(
			'contact',
			array(
				'name' => $_POST['input_name'],
				'email' => $_POST['input_email'],
				'content' => $_POST['input_content'],
				'created' => $current_datetime
			),
			array(
				'%s',
				'%s',
				'%s',
				'%s'
			)
		);

		if( !empty($wpdb->insert_id) ) {

			$page_flag = 3;

			if( !empty($_SESSION['token']) ) {
				unset($_SESSION['token']);
			}

		}
	}

}

----- 省略 -----

ここまでの変更を保存して、ブラウザでお問い合わせページを開いてもう一度入力ページから完了ページまで入力を進めてみてください。
もし完了ページでブラウザの再読み込みをしても、データを登録後にセッショントークンを削除しているためvalidation関数でエラーとなり入力ページへ戻ります。

今回でお問い合わせページは一通り完成しました。
これでお問い合わせページから入力したデータがデータベースに保存されるようになったため、プラグインからデータを参照したり編集する準備が整いました。

今回作成したコード:GitHub

こちらの記事は役に立ちましたか?

ありがとうございます。
もしよろしければ、あわせてフィードバックや要望などをご入力ください。

コメントありがとうございます!
運営の参考にさせていただきます。

前のページへ 一覧に戻る 次のページへ