最近こんなツイートをしました。もう少し詳しく説明していきます。
PHPのセキュリティ対策について
— Tomoya (@div_tomo) August 20, 2023
【XSS】
・表示の際にHTMLエスケープすることを徹底
・信頼できない値を出力するコンテキストによって、エスケープ方法が異なる。
【CSRF】
・トークンを埋め込んで接続元を判定
・トークンの生成には、暗号論的擬似乱数生成器を使用#PHP #セキュリティ #XSS #CSRF
この記事ではセキュリティ対策の記事を作成しています。
お問い合わせフォームの実装についての記事
【PHP】①お問い合わせフォーム 実装
バリデーションについての記事
【PHP】③お問い合わせフォーム バリデーション
※[【PHP】①お問い合わせフォーム 実装 ] も同時に実施すると理解度が深まります。
サイトへの攻撃
💡 XSS(Cross-Site Scripting) CSRF などXSSについて
<>
や ’’
などを無効にする処理をしていきます。
****htmlspecialchars()****
この関数を使って &
や >
などに変換して、<>
や ‘’
などを無効化にすることができます。以下に表を記載しておきます。
変更対象となる文字
変換前 | 変換後 |
---|---|
& (アンパサンド) | & |
" (ダブルクォート) | ENT_NOQUOTES が指定されていない場合、" |
' (シングルクォート) | ' (ENT_HTML401 の場合) あるいは ' ( ENT_XML1、ENT_XHTML、 ENT_HTML5 の場合)。ただし ENT_QUOTES が指定されている場合に限る |
< (小なり) | < |
> (大なり) | > |
書き方
return htmlspecialchars($_POST['name'], ENT_QUOTES, 'UTF-8');
$_POST['name']
は文字を指定
ENT_QUOTES
シングルクオートとダブルクオートを共に変換
UTF-8
は文字コードを指定
CSRFについて
認証されたユーザーを騙し、知らぬ間に悪意のあるリクエストを送信して、意図しない処理が行われ実行させられてしまう攻撃
フォームを作る際は必ずこのCSRF対策をしていく必要があります。
$_SESSION
$_GET
や $_POST
は送信すると1回きりで消えてしまうのですが、 $_SESSION
の場合はずっと残ってくれます。
書き方
session_start();
コードの冒頭に session_start();
と記載してあげればセッションを使うことができます。
暗号の作り方
<?php random_bytes(32); ?>
こちらの関数は、安全なバイト列を生成してくれます。
しかしこのままではうまく使えないので16進数に書き換えていきます。
<?php bin2hex(random_bytes(32)); ?>
手順
セキュリティ対策の実装
[ 【PHP】①お問い合わせフォーム 実装 ] のコードを元にセキュリティ対策を書いていきます。
▼長いのでCtrl+F
で「追加」と検索して調べると追加された箇所を確認できます。
form > input.php
<?php
session_start(); //追加
$pageFlag = 0;
// 入力画面の値が空では無かったら$pageFlagを1に変更
if(!empty($_POST['btn_confirm'])){
$pageFlag = 1;
}
// 送信画面の値が空では無かったら$pageFlagを2に変更
if(!empty($_POST['btn_submit'])){
$pageFlag = 2;
}
// 追加
function hsc($str)
{
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE html>
<meta charset="utf-8">
<head></head>
<body>
<!-- 入力画面 -->
<?php if($pageFlag === 0) : ?>
<!-- 追加 -->
<?php
if(!isset($_SESSION['csrfToken']))
{
$csrfToken = bin2hex(random_bytes(32));
$_SESSION['csrfToken'] = $csrfToken;
}
$token = $_SESSION['csrfToken'];
?>
<form method="POST" action="input.php">
氏名
<input type="text" name="name" value="<?php if(!empty($_POST['name'])){ echo hsc($_POST['name']) ; } ?>"><br> <!-- 追加 -->
メールアドレス
<input type="email" name="email" value="<?php if(!empty($_POST['email'])){ echo hsc($_POST['email']) ; } ?>"><br> <!-- 追加 -->
<input type="submit" name="btn_confirm" value="確認する">
<input type="hidden" name="csrf" value="<?php echo $token; ?>"> <!-- 追加 -->
</form>
<?php endif; ?>
<!-- 確認画面 -->
<?php if($pageFlag === 1) : ?>
<?php if($_POST['csrf'] === $_SESSION['csrfToken']) : ?> <!-- 追加 -->
<form method="POST" action="input.php">
氏名
<?php echo hsc($_POST['name']) ;?> <!-- 追加 -->
<br>
メールアドレス
<?php echo hsc($_POST['email']) ;?> <!-- 追加 -->
<br>
<input type="submit" name="back" value="戻る">
<input type="submit" name="btn_submit" value="送信する">
<input type="hidden" name="name" value="<?php echo hsc($_POST['name']) ;?>"> <!-- 追加 -->
<input type="hidden" name="email" value="<?php echo hsc($_POST['email']) ;?>"> <!-- 追加 -->
<input type="hidden" name="csrf" value="<?php echo hsc($_POST['csrf']) ;?>"> <!-- 追加 -->
</form>
<?php endif; ?> <!-- 追加 -->
<?php endif; ?>
<!-- 完了画面 -->
<?php if($pageFlag === 2) : ?>
<?php if($_POST['csrf'] === $_SESSION['csrfToken']) : ?> <!-- 追加 -->
<p>送信が完了しました。</p>
<?php unset($_SESSION['csrfToken']); ?> <!-- 追加 -->
<?php endif; ?> <!-- 追加 -->
<?php endif; ?>
</body>
</html>
セッションの使用
コードの冒頭に session_start();
と記載してあげればセッションを使うことができます。
XSSを関数にする
function hsc($str)
{
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
$str
は文字を指定
ENT_QUOTES
シングルクオートとダブルクオートを共に変換
UTF-8
は文字コードを指定
1つ1つにhtmlspecialchars()
を書いていくのは面倒なので今回はfunction()
関数にしています。
作ったトークンを変数に入れる
<?php $csrfToken = bin2hex(random_bytes(32)); ?>
random_bytes()
こちらの関数は、安全なバイト列を生成してくれます。
しかしこのままではうまく使えないので bin2hex()
で16進数に書き換えていきます。
作ったトークンを変数 $csrfToken
に入れてあげています。
この状態だと入力画面を表示するたびにトークンを作成してしまうので今回はif文で $csrfToken
が無かったら作成するという処理を加えています。
if(!isset($_SESSION['csrfToken']))
{
$csrfToken = bin2hex(random_bytes(32));
//キー //バリュー
$_SESSION['csrfToken'] = $csrfToken;
}
$token = $_SESSION['csrfToken'];
csrfToken
が設定されていなかったら $csrfToken
を作るようにしています。
$_SESSION['csrfToken'];
だと少し長いので変数に入れています。
作った関数を使用する
氏名
<input type="text" name="name" value="<?php if(!empty($_POST['name'])){ echo hsc($_POST['name']) ; } ?>"><br> <!-- 追加 -->
メールアドレス
<input type="email" name="email" value="<?php if(!empty($_POST['email'])){ echo hsc($_POST['email']) ; } ?>"><br> <!-- 追加 -->
function
で作った関数hsc()
を使っている
作ったトークンを仕込んでいく
<input type="hidden" name="csrf" value="<?php echo $token; ?>">
これで value に合言葉が入るようになりました。
トークンが正しいか判定
if($_POST['csrf'])
’csrf’は入力画面のformからとってくる値です。
<?php if($_POST['csrf'] === $_SESSION['csrfToken']) ?>
で送られてくるトークンと送ったトークンがあっているかの判定をしています。
なのでトークンが同じであれば確認画面を表示するというような処理にしています。
作った関数を使用する
氏名
<?php echo hsc($_POST['name']) ;?> <!-- 追加 -->
<br>
メールアドレス
<?php echo hsc($_POST['email']) ;?> <!-- 追加 -->
<br>
function
で作った関数hsc()
を使っている
作った関数を使用する
<input type="hidden" name="name" value="<?php echo hsc($_POST['name']) ;?>"> <!-- 追加 -->
<input type="hidden" name="email" value="<?php echo hsc($_POST['email']) ;?>"> <!-- 追加 -->
function
で作った関数hsc()
を使っている
csrfもhiddenで持たせてあげる
<input type="hidden" name="csrf" value="<?php echo hsc($_POST['csrf']) ;?>">
これで消えずに持たせてあげることができました。
完了画面でもトークンが正しいか判定
<?php if($_POST['csrf'] === $_SESSION['csrfToken']) ?>
で送られてくるトークンと送ったトークンがあっているかの判定をしています。
トークンの削除
<?php unset($_SESSION['csrfToken']); ?>
トークンがずっと残っているとよろしくないのでトークンを削除する処理を記載しています。
この流れでお問い合わせフォームのバリデーションについての記事を読む
【PHP】③お問い合わせフォーム バリデーション処理