[PHP]XSS、クリックジャッキング、CSRFの対策方法

#はじめに
PHPのセキュリティ対策についてアウトプットすることで知識の定着を図るためにこの記事を作成しました。
間違いがありましたらご指摘をいただけると幸いです。
XSS、クリックジャッキング、CSRFについて説明した後に、それらへの対策を施した入力フォームのコードを読んでいってます。

#XSS(クロスサイトスクリプティング)

外部からの入力に応じて表示が変化するサイトなどを作る時にHTML生成の実装に問題があると、外部からスクリプトを埋め込まれてクッキーを盗まれたり、JSによる攻撃を受けてしまいます。
このような攻撃手法をXSSと言います。

####XSSの脆弱性の確認方法
`<script>alert('攻撃可能');</script>`
上記の文を入力フォームに入力して確認することができます。
アラートが出るとJSで仕込むことができてしまうということなので危ない状態です。

####対策方法
一般的な対策方法としては、`<`や`"`を文字参照によってエスケープするのが基本です。
*[文字参照とは](https://www.tagindex.com/html_tag/basic/character.html)


XSSを回避する方法は、下記のような関数を設定して特殊文字をHTMLエンティティに変換します。

```sample.php
<?php
function h($str) {
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>
```
*[htmlspecialcharsとは](https://www.php.net/manual/ja/function.htmlspecialchars.php)

この`function h();`を入力画面や確認画面などにあるvalueに設定するとアラートが出ないようになります。
EX)`<?php echo h($_POST['email']); ?>`

#クリックジャッキング
クリックするボタンを乗っとる攻撃方法です。
EX)
ユーザーに表示されているページの上にiframeでさらにページを重ねて、その重ねたページを透明にすることでユーザーには重なっている下のページだけが見えています。上の透明なページに悪意のあるサイトに誘導するようなボタンを設置するなどしています。

####対策方法
PHPファイルに直接記述する方法。
header関数で`X-FRAME-OPTIONS: DENY`を指定してHTTPヘッダーに送ります。そうすると重ねて表示することができなくなります。

#CSRF(クロスサイトリクエストフォージェリ)
投稿やメールの送信、商品の購入などの重要な処理を、外部から実行させる攻撃手法です。

####対策方法
重要な処理に対するリクエストが利用者の意図するものかどうかを判断する必要があります。
つまり、①CSRF対策が必要なページを見極める②利用者が意図したリクエストか見極める
正規のリクエストかどうか判断する方法としては、①秘密情報(トークン)を埋め込む②パスワードの再入力

$_GET、$_POSTはデータをやり取りすると１回で情報が消えてしまいます。
$_SESSIONは、データを残すことができます。
この$_SESSIONを使ってトークンを発行します。

`session_start();`でセッションを使う宣言をします。

`bin2hex(random_bytes(32));`

`random_bytes`は、 暗号論的に安全な、疑似ランダムなバイト列を生成する関数です。
`bin2hex`は、バイナリのデータを16進数表現に変換する関数です。

`random_bytes`単体だと、
h�k����z!�z�[1Õ֥�� �51�[ې^
などの意味のわからないトークンを生成してしまうので、`bin2hex`を使って16進数表現に変換します。

入力画面→確認画面→完了画面というページの遷移の場合、入力画面でトークンを発行して`input type='hidden'`で表示せずにPOSTメソッドを使って確認ページに送信します。
そして、確認ページで`if ($_POST['csrf'] === $_SESSION['csrf']):`として、トークンが一致すればページを表示するようにします。
確認画面から完了画面に関しても同じように実装します。
いつまでも、セッションが残っているのは良くないので完了画面で`unset($_SESSION['csrf'])`として特定のセッションを削除しましょう。


##入力フォームのコードでセキュリティ対策を読む
コメントでコードを読んでいきます。

```sample.php
<?php
//セッションの始まりの宣言
session_start();
//クリックジャッキングの対策。header関数でHTTPヘッダーにページをフレーム内に表示できないような設定を送っています。
header("X-FRAME-OPTIONS: DENY");
//htmlspecialchars関数で特殊文字を変換しています。ENT_QUOTESで、シングルクオートとダブルクオート両方変換するように設定しています。
function h($str) {
  return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

$page_flag = 0;
//もし、空でないなら処理
if (!empty($_POST['button_confirm'])) {
  $page_flag = 1;
}
//もし、空でないなら処理
if (!empty($_POST['button_submit'])) {
  $page_flag = 2;
}
?>

<!DOCTYPE html>
<meta charset="UTF-8">
<head></head>
<body>


<?php if($page_flag === 1): ?>
<!-- $_POSTで送られてきたトークンと$_SESSIONで残っているトークンが一致すれば表示 -->
<?php if ($_POST['csrf'] === $_SESSION['csrf_token']): ?>
<!-- POSTメソッドでURLに表示されない形でsample2.phpに送信 -->
<form method="POST" action="sample2.php">

名前

<!-- h()で特殊文字を変換している -->
<?php echo h($_POST['your_name']); ?>
<br>

メールアドレス
<!-- h()で特殊文字を変換している -->
<?php echo h($_POST['email']); ?>

<input type="submit" name="back" value="戻る">
<input type="submit" name="button_submit" value="送信する">
<!-- hiddenで情報を保持している。h()で特殊文字を変換している -->
<input type="hidden" name="your_name" value="<?php echo h($_POST['your_name']); ?>">
<input type="hidden" name="email" value="<?php echo h($_POST['email']); ?>">
<input type="hidden" name="csrf" value="<?php echo h($_POST['csrf']); ?>">

</form>

<?php endif; ?>
<?php endif; ?>

<?php if($page_flag === 2): ?>
<!-- $_POSTで送られてきたトークンと$_SESSIONで残っているトークンが一致すれば表示 -->
<?php if ($_POST['csrf'] === $_SESSION['csrf_token']): ?>

送信が完了しました。
<!-- 特定のセッションを削除している -->
<?php unset($_SESSION['csrf_token']); ?>
<?php endif; ?>
<?php endif; ?>


<?php if($page_flag === 0): ?>
<?php
//もしトークンがセットされていなければ、random_bytesでトークンを生成し、bin2hexで16進数に変換している。それを$_SESSIONに代入して、さらに変数に代入している。 -->
if(!isset($_SESSION['csrf_token'])) {
$csrf_token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $csrf_token;
}
$token = $_SESSION['csrf_token'];
?>

<!-- POSTメソッドでURLに表示されない形でsample2.phpに送信 -->
<form method="POST" action="sample2.php">

名前
<!-- h()で特殊文字を変換している -->
<input type="text" name="your_name" value="<?php echo h($_POST['your_name']); ?>"></input>
<br>

メールアドレス
<!-- h()で特殊文字を変換している -->
<input type="email"  name="email" value="<?php echo h($_POST['email']); ?>"></input>
<input type="submit" name="button_confirm" value="確認する">
<!-- hiddenでトークンを保持している。h()で特殊文字を変換している -->
<input type="hidden" name="csrf" value="<?php echo h($token); ?>">
</form>

<?php endif; ?>
```

#最後に
この記事を読んでいただきありがとうございました。