PHPチャットアプリのセキュリティ強化：XSSとCSRF攻撃への対策

## 目次

- [記事の内容](#記事の内容)
- [本編](#本編)
  - [XSS攻撃による攻撃](#xss攻撃による攻撃)
  - [CSRF攻撃による攻撃](#csrf攻撃による攻撃)
- [終わりに](#終わりに)

## 記事の内容
以前執筆した記事[ゼロから始めるPHP：チャットアプリ開発入門](https://qiita.com/fwzis/items/5fa0e33da3484b5fde70)で、生のPHPを用いたシンプルなチャットアプリを作成しました。ただ、そのチャットアプリはセキュリティ面において完全に安全ではありませんでした。なので勉強用として、今回の記事ではこのチャットアプリを更にセキュリティ上安全なものに改良することを目指します。
しかしながら、私自身もセキュリティに関してとても詳しいわけではないので、記事の内容に完璧な保証をすることはできません。もし改善の余地がある部分や間違いがある場合は、どんな小さな情報でも構いませんのでコメントにてお知らせください。

## 本編
今後内容を追記していく可能性があるが、今回はXSS攻撃とCSRF攻撃の例とその解決策について記述する。

### XSS攻撃による攻撃
XSS攻撃とは、悪意のあるスクリプトをWebページに送り込み、それを多数のユーザーによって実行される可能性があるものである。例えば掲示板のような、ページでユーザーが入力した内容を多数の人が閲覧できるページにおいて、悪意のあるスクリプトをその入力に含ませるというようなこともできる。
作成したチャットアプリに対してこの攻撃を行う方法は多数存在していると思うが、例えば以下のようなHTMLフォームを通じて、XSS攻撃を行うことができる。

```html
<!DOCTYPE html>
<html>
<head>
    <title>XSS攻撃</title>
    <meta charset="utf-8">
</head>
<body>
    <form action="http://localhost/update_profile.php" method="post" style="width: 100%;">
        <input name="name" value="<script>alert('XSS攻撃をされました');</script>" hidden>
        <input name="password" value="password" hidden>
        <input name="confirm_password" value="password" hidden>
        <button type="submit" style="display: block; margin: 0 auto; margin-top: 200px; height: 100px; width: 300px; font-size: 36px">今すぐ押せ！！</button>
    </form>
</body>
</html>
```
#### 問題点
全く関係のないサーバー上でこのHTMLが表示され、ボタンを押してしまえばフォームに含まれる`<script>`タグによって、攻撃者による不正なスクリプトの埋め込みを可能にしている。この例では、ユーザーがすでにチャットアプリにログイン済みの時、ユーザー名として表示される部分に危険なスクリプトが挿入される。このような手法によって、最悪の場合ユーザーの個人情報が流出してしまう危険性につながる。

#### 画面表示
HTMLは以下のような画面表示となっていて、一見何のことか分からない。
![スクリーンショット 2023-07-29 14.28.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/9261615c-6df6-faf6-f51b-c70aceac1c09.png)

もし押してしまったら、以下のような画面が表示される。
![スクリーンショット 2023-07-29 15.26.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/1a76f38a-ba33-f056-b480-8c0b5dcef198.png)

Webページ上でユーザー名が表示されている数だけこのスクリプトが表示されてしまう。これは、全く別のサーバーのHTMLから意図しないスクリプトを実行させられていることを表している。

#### 解決策
これはフォームに入力されたスクリプトがそのまま埋め込まれてしまうことが原因であるため、フォームからのユーザー入力のHTMLやスクリプトをエスケープすることで、不正なスクリプトなどを無効化することが必要である。
具体的には、htmlspecialchars()やhtmlentities()を用いることで意味のある文字をエスケープすることができる。ちなみにhtmlspecialchars()はHTMLタグなどコードの一部として機能するものが変換対象となっている。ここではhtmlspecialchars()を使用する。
phpのinput部分に以下のような記述すると良い。
```php
<p>ユーザー名：<?= htmlspecialchars($_SESSION["name"], ENT_QUOTES, 'UTF-8'); ?></p>
```
これはindex.phpの一部だが、こうすることでユーザー情報のユーザー名表示の部分に、スクリプトコードがそのまま表示される。それによって勝手にこのスクリプトコードが実行されてしまうことを防いでいる。また、チャットアプリにも名前が表示されるため、その部分にもhtmlspecialchars()を使用する必要がある。
![スクリーンショット 2023-07-29 15.52.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/ec85cc8e-714e-58c8-1076-bb0e166c142e.png)



### CSRF攻撃による攻撃
CSRF攻撃は、攻撃対象サーバ上のWebアプリケーションは不正なリクエストを処理し、ユーザが意図していない処理を行わせるものである。こちらも具体例として以下のHTMLを用意した。

```html
<!DOCTYPE html>
<html>

<head>
    <title>CSRF攻撃</title>
    <meta charset="utf-8">
</head>

<body>
    <form action="http://localhost/send_message.php" method="post" style="width: 100%;">
        <input name="message" value="csrf攻撃されてますよ" hidden>
        <button type="submit" style="display: block; margin: 0 auto; margin-top: 200px; height: 100px; width: 300px; font-size: 36px">今すぐ押せ！！</button>
    </form>
</body>

</html>
```
#### 問題点
全く関係のないサーバー上でこのHTMLが表示され、Webページにログインした状態でボタンを押してしまえば、ユーザー名を任意のスクリプトに変更されてしまう。こちらもXSS攻撃と同様にとても危険なものである。
#### 画面表示
こちらのHTMLも以下のような画面表示となっていて、一見何のことか分からない。
![スクリーンショット 2023-07-29 14.28.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/9261615c-6df6-faf6-f51b-c70aceac1c09.png)

もし押してしまったら、以下のような画面が表示される。
![スクリーンショット 2023-07-29 16.06.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/921cf892-b5f6-f38a-a7d9-912f3402763b.png)

勝手に意図しないメッセージが送信されてしまっている。twitterで同じことが起こることを考えたらどれだけ危険かがわかるだろう。

#### 解決策
GET通信によって得られるトークンを利用しないとPOST通信を受け付けないようにすれば良い。
具体的には以下のコードを用いて、セッションにtokenという名前で保存する。トークンは64byteのランダムな文字列として生成する。
```php
$_SESSION["token"] = bin2hex(random_bytes(32));
```
index.phpのセッション開始部分に以下の記述を追記する。
```diff_php
<?php
// ログインしていない場合はログインページにリダイレクト
session_start();
if (!isset($_SESSION["id"])) {
    header("Location: login.php");
    exit;
}
+ $_SESSION["token"] = bin2hex(random_bytes(32));
?>
```
こうすることで、GET通信でこのフォームを受け取らなければ、トークンを受け取ることはできない。そしてこの値を用いて、フォームを送信し、POST通信で送られたトークンと、セッションに保存されているトークンが一致していればPOST通信を受け入れる形にすれば良い。
index.phpのメッセージ送信部分を以下のように変更する。
```diff_php
<form action="send_message.php" method="post" class="chat-form">
                <label for="message">メッセージを入力してください:</label>
                <input type="text" id="message" name="message" required>
+               <input type="text" name="token" value=<?= $_SESSION["token"]?> hidden>
                <button type="submit">送信</button>
            </form>
```
また、send_message.phpを以下のように変更する。
```diff_php
<?php
session_start();
include "sql_connection.php";

$message = $_POST["message"];
+$token = $_POST["token"];
+if ($token != $_SESSION["token"]) {
+    die("不正な送信がありました。");
+}
$users_id = $_SESSION["id"];

$stmt = $conn->prepare("INSERT INTO messages (message, users_id) VALUES (?, ?)");
$stmt->bind_param("si", $message, $users_id);
$stmt->execute();

header("Location: index.php");
exit;
```
これで、GET通信で得たフォームからのPOST通信しか受け入れなくなった。試しに以下のHTML文でCSRF攻撃をしてみる。トークンは分からないので適当に入れるしかない。
```diff_html
<!DOCTYPE html>
<html>

<head>
    <title>CSRF攻撃</title>
    <meta charset="utf-8">
</head>

<body>
    <form action="http://localhost/send_message.php" method="post" style="width: 100%;">
        <input name="message" value="csrf攻撃されてますよ" hidden>
+       <input name="token" value="tokenが分からない" hidden>
        <button type="submit" style="display: block; margin: 0 auto; margin-top: 200px; height: 100px; width: 300px; font-size: 36px">今すぐ押せ！！</button>
    </form>
</body>

</html>
```
すると以下のような画面表示になり、勝手に送信されることはなくなった。
![スクリーンショット 2023-07-29 16.41.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3509941/dbba78d1-db0b-7b68-2700-6de1c644e8d7.png)


## 終わりに
この記事では以前作成したPHPチャットアプリのセキュリティ上の脆弱性に焦点を当て、XSS攻撃とCSRF攻撃に対する対策を紹介しました。XSS攻撃では不正なスクリプトを実行させる危険性があり、CSRF攻撃ではユーザーが意図しない処理を行わせる危険性があります。それらの脆弱性を解消するために、フォーム入力のエスケープやトークンの利用などの対策を取り入れました。しかし、セキュリティに対する完璧な保証はありません。セキュリティ向上のためにも、さらなる検証と改善を行う姿勢が重要だと思います(ちなみにLaravelではこれらの対策を自動で行ってくれます)。記事の内容に関して皆さんからのコメントやフィードバックをお待ちしています。ここまでお読みいただき、ありがとうございました。