【PHP】②お問い合わせフォーム セキュリティ対策

最近こんなツイートをしました。もう少し詳しく説明していきます。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">PHPのセキュリティ対策について<br><br>【XSS】<br>・表示の際にHTMLエスケープすることを徹底<br>・信頼できない値を出力するコンテキストによって、エスケープ方法が異なる。<br><br>【CSRF】<br>・トークンを埋め込んで接続元を判定<br>・トークンの生成には、暗号論的擬似乱数生成器を使用<a href="https://twitter.com/hashtag/PHP?src=hash&amp;ref_src=twsrc%5Etfw">#PHP</a> <a href="https://twitter.com/hashtag/%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3?src=hash&amp;ref_src=twsrc%5Etfw">#セキュリティ</a> <a href="https://twitter.com/hashtag/XSS?src=hash&amp;ref_src=twsrc%5Etfw">#XSS</a> <a href="https://twitter.com/hashtag/CSRF?src=hash&amp;ref_src=twsrc%5Etfw">#CSRF</a></p>&mdash; Tomoya (@div_tomo) <a href="https://twitter.com/div_tomo/status/1693280214917456217?ref_src=twsrc%5Etfw">August 20, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

この記事ではセキュリティ対策の記事を作成しています。

#### お問い合わせフォームの実装についての記事
【PHP】①お問い合わせフォーム 実装

https://qiita.com/div_tomo/private/f3603b8775a9e2fa72b8

#### バリデーションについての記事
【PHP】③お問い合わせフォーム バリデーション

https://qiita.com/div_tomo/private/96d2f34cccd6f0bdca12

#### ※[【PHP】①お問い合わせフォーム 実装 ] も同時に実施すると理解度が深まります。
https://qiita.com/div_tomo/private/f3603b8775a9e2fa72b8
### サイトへの攻撃

<aside>
💡 XSS（Cross-Site Scripting）
CSRF
など

</aside>

## XSS****について****

`<>` や `’’` などを無効にする処理をしていきます。

```php
****htmlspecialchars()****
```

この関数を使って `&amp;` や `&gt;` などに変換して、`<>` や `‘’` などを無効化にすることができます。以下に表を記載しておきます。

**変更対象となる文字**

| 変換前 | 変換後 |
| --- | --- |
| & (アンパサンド) | &amp; |
| " (ダブルクォート) | ENT_NOQUOTES が指定されていない場合、&quot; |
| ' (シングルクォート) | &#039; (ENT_HTML401 の場合) あるいは &apos; ( ENT_XML1、ENT_XHTML、 ENT_HTML5 の場合)。ただし ENT_QUOTES が指定されている場合に限る |
| < (小なり) | &lt; |
| > (大なり) | &gt; |

### 書き方

```php
return htmlspecialchars($_POST['name'], ENT_QUOTES, 'UTF-8');
```

`$_POST['name']` は文字を指定

`ENT_QUOTES` シングルクオートとダブルクオートを共に変換

`UTF-8` は文字コードを指定

## CSRFについて

**認証されたユーザーを騙し、知らぬ間に悪意のあるリクエストを送信して、意図しない処理が行われ実行させられてしまう攻撃**

フォームを作る際は必ずこのCSRF対策をしていく必要があります。

```php
$_SESSION
```

 `$_GET` や `$_POST` は送信すると１回きりで消えてしまうのですが、 `$_SESSION` の場合はずっと残ってくれます。

### 書き方

```php
session_start();
```

コードの冒頭に `session_start();` と記載してあげればセッションを使うことができます。

### 暗号の作り方

```php
<?php random_bytes(32); ?>
```

こちらの関数は、安全なバイト列を生成してくれます。

しかしこのままではうまく使えないので１６進数に書き換えていきます。

```php
<?php bin2hex(random_bytes(32)); ?>
```

# 手順

## セキュリティ対策の実装

[ ****【PHP】①お問い合わせフォーム 実装 ]  のコードを元にセキュリティ対策を書いていきます。****

▼長いので`Ctrl+F`で「**追加**」と検索して調べると追加された箇所を確認できます。

> form > input.php
> 

```php
<?php

session_start(); //追加

$pageFlag = 0;
// 入力画面の値が空では無かったら$pageFlagを１に変更
if(!empty($_POST['btn_confirm'])){
    $pageFlag = 1;
}
// 送信画面の値が空では無かったら$pageFlagを２に変更
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を関数にする

```php
function hsc($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
```

`$str` は文字を指定

`ENT_QUOTES` シングルクオートとダブルクオートを共に変換

`UTF-8` は文字コードを指定

１つ１つに`htmlspecialchars()` を書いていくのは面倒なので今回は`function()`関数にしています。

### 作ったトークンを変数に入れる

```php
<?php $csrfToken = bin2hex(random_bytes(32)); ?>
```

`random_bytes()` こちらの関数は、安全なバイト列を生成してくれます。

しかしこのままではうまく使えないので `bin2hex()` で１６進数に書き換えていきます。

作ったトークンを変数 `$csrfToken` に入れてあげています。

この状態だと入力画面を表示するたびにトークンを作成してしまうので今回はif文で `$csrfToken` が無かったら作成するという処理を加えています。

```php
if(!isset($_SESSION['csrfToken']))
{
    $csrfToken = bin2hex(random_bytes(32));
		//キー               //バリュー
		$_SESSION['csrfToken'] = $csrfToken;
}
$token = $_SESSION['csrfToken'];
```

`csrfToken` が設定されていなかったら `$csrfToken` を作るようにしています。

`$_SESSION['csrfToken'];` だと少し長いので変数に入れています。

### 作った関数を使用する

```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> <!-- 追加 -->
```

`function`で作った関数`hsc()`を使っている

### 作ったトークンを仕込んでいく

```php
<input type="hidden" name="csrf" value="<?php echo $token; ?>">
```

これで value に合言葉が入るようになりました。

### トークンが正しいか判定

`if($_POST['csrf'])` ’csrf’は入力画面のformからとってくる値です。

```php
<?php if($_POST['csrf'] === $_SESSION['csrfToken']) ?> 
```

で送られてくるトークンと送ったトークンがあっているかの判定をしています。

なのでトークンが同じであれば確認画面を表示するというような処理にしています。

### 作った関数を使用する

```php
氏名
<?php echo hsc($_POST['name']) ;?> <!-- 追加 -->
<br>

メールアドレス
<?php echo hsc($_POST['email']) ;?> <!-- 追加 -->
<br>
```

`function`で作った関数`hsc()`を使っている

### 作った関数を使用する

```php
<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で持たせてあげる

```php
<input type="hidden" name="csrf" value="<?php echo hsc($_POST['csrf']) ;?>">
```

これで消えずに持たせてあげることができました。

### 完了画面でもトークンが正しいか判定

```php
<?php if($_POST['csrf'] === $_SESSION['csrfToken']) ?> 
```

で送られてくるトークンと送ったトークンがあっているかの判定をしています。

### トークンの削除

```php
<?php unset($_SESSION['csrfToken']); ?>
```

トークンがずっと残っているとよろしくないのでトークンを削除する処理を記載しています。

#### この流れでお問い合わせフォームのバリデーションについての記事を読む
【PHP】③お問い合わせフォーム バリデーション処理

https://qiita.com/div_tomo/private/96d2f34cccd6f0bdca12