htmlspecialchars関数やhtmlentities関数で使用されるフラグの検証

# **導入**

**[htmlspecialchars](http://www.php.net/manual/ja/function.htmlspecialchars.php)** 関数や **[htmlentities](http://www.php.net/manual/ja/function.htmlspecialchars.php)** 関数で使用可能なフラグとして以下のものがありますが、詳しい違いを考慮したことが無かったので、ここで検証してみることにします。

![ss (2014-05-08 at 06.03.55).png](https://qiita-image-store.s3.amazonaws.com/0/25060/0b75c35a-6edb-eef1-28ff-62231cac1d36.png)

# **検証**

## 定数を2進数表示してみる

```php:コード
$types = [
    'ENT_COMPAT',  'ENT_QUOTES',     'ENT_NOQUOTES',
    'ENT_IGNORE',  'ENT_SUBSTITUTE', 'ENT_DISALLOWED',
    'ENT_HTML401', 'ENT_XML1',       'ENT_XHTML',      'ENT_HTML5',
];
foreach ($types as $type) {
    printf('% 14s: %08b' . PHP_EOL, $type, constant($type));
}
```

```txt:実行結果
    ENT_COMPAT: 00000010
    ENT_QUOTES: 00000011
  ENT_NOQUOTES: 00000000
    ENT_IGNORE: 00000100
ENT_SUBSTITUTE: 00001000
ENT_DISALLOWED: 10000000
   ENT_HTML401: 00000000
      ENT_XML1: 00010000
     ENT_XHTML: 00100000
     ENT_HTML5: 00110000
```

この結果から各桁に関して以下のことが推測できます。マニュアルの分かりにくい部分は読み替えています。

1. 処理対象に `'` を含める
2. 処理対象に `"` を含める (デフォルトで有効)
3. 指定 **文字コード** として無効なシーケンスを削除する
4. 指定 **文字コード** として無効なシーケンスを同じバイト長の `�` で構成される文字列に置換する
5. 文書タイプ指定 下位ビット
6. 文書タイプ指定 上位ビット
7. 未実装
8. 指定 **文書タイプ** として無効なシーケンスを同じバイト長の `�` で構成される文字列に置換する

### 文書タイプ指定

| 2進数値 | 文書タイプ |
|:----:|:---:|
| `00`<br />(デフォルト) | HTML |
| `01` | XML |
| `10` | XHTML |
| `11` | HTML5 |

## `'` `"` の扱いに関して

基本動作は `ENT_COMPAT` `ENT_QUOTES` `ENT_NOQUOTES` に関連するマニュアルの記載通りに自明です。

```php:コード
function test($string, $type) {
    return htmlspecialchars($string, ENT_HTML401 | constant($type), 'UTF-8');
}
foreach (['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'] as $type) {
    printf('% 12s: %s' . PHP_EOL, $type, test('"\'', $type));
}
```

```html:実行結果
  ENT_COMPAT: &quot;'
  ENT_QUOTES: &quot;&#039;
ENT_NOQUOTES: "'
```


関連する関数のデフォルトのフラグは `ENT_COMPAT | ENT_HTML401` となっていますが、ビット演算の性質上、文書タイプのみを指定したときには `ENT_NOQUOTES` が適用されてしまうことに注意してください。

```php:コード
echo htmlspecialchars('"\'', ENT_HTML401, 'UTF-8');
```

```html:実行結果
"'
```


もう1点気になるのは、 `'` のみを対象とするフラグが存在しないことです。なので、実際に2進数 `01` を直接渡したときにどのような結果になるのかを検証してみました。

```php:コード
echo htmlspecialchars('"\'', 1, 'UTF-8');
```

```html:実行結果
"&#039;
```

定数としては定義されていませんが、期待した通りの結果が得られました。

## 文書タイプに関して

**[get_html_translation_table](http://www.php.net/manual/ja/function.get-html-translation-table.php)** 関数の返り値を比較することで検証を試みましたが、出力結果が長すぎるので要点だけ記載します。文字コードによって結果が異なるようなので、ここでは `UTF-8` だけを対象に考えます。

### 共通

`<` `>` `&` `"` はそれぞれ `&lt;` `&gt;` `&amp;` `&quot;` と対応する

### **[htmlspecialchars](http://www.php.net/manual/ja/function.htmlspecialchars.php)** 関数 / **[htmlspecialchars_decode](http://www.php.net/manual/ja/function.htmlspecialchars-decode.php)** 関数

| | `ENT_XML1` | `ENT_HTML401`<br />`ENT_XHTML` | `ENT_HTML5` |
|:---:|:-----------:|:------------------------------:|:----------:|
| `'` との対応 | `&apos;` | `&#039;` | `&#039;` |
| ギリシャ文字など | × | × | × |
| タブ・改行など | × | × | × |

### **[htmlentities](http://www.php.net/manual/ja/function.htmlentities.php)** 関数 / **[html_entity_decode](http://www.php.net/manual/ja/function.html-entity-decode.php)** 関数

| | `ENT_XML1` | `ENT_HTML401`<br />`ENT_XHTML` | `ENT_HTML5` |
|:---:|:-----------:|:------------------------------:|:----------:|
| `'` との対応 | `&apos;` | `&#039;` | `&#039;` |
| ギリシャ文字など | × | ○ | ○ |
| タブ・改行など | × | × | ○ |

XHTMLはHTMLともXMLとも考えられますが、互換性を考慮してHTML寄りの実装になっていると思われます。しかし

```php
header('Content-Type: application/xhtml+xml; charset=utf-8');
``` 

としてヘッダーを直接送出する場合、そもそも互換性の無いブラウザは正常にページを表示することが出来ないので、XMLとして扱わせておく方が妥当な気はします。 

<strong>【追記】</strong>
<b>`&apos;`をデコードしたいときは，`ENT_HTML401`以外の文書タイプを`ENT_QUOTES`とともに使う必要がある．</b>

## <ins>文字コードとして</ins>無効なシーケンスの扱い

`ENT_IGNORE` と `ENT_SUBSTITUTE` は互いに排反的な機能に対応するフラグですが、 **[php_escape_html_entities_ex](https://github.com/php/php-src/blob/master/ext/standard/html.c#L1226)** 関数の実装によれば両方のフラグを設定した場合は `ENT_IGNORE` の方が優先されるようです。

### `ENT_IGNORE` 指定

無効なシーケンスは **削除** されます。しかし、以下のようなコードを書いてしまうとかえってXSS脆弱性を作り込む要因となってしまいます。

そもそも **[「XSSの攻撃手法いろいろ」](http://d.hatena.ne.jp/unageanu/20100619/1276921005)** を見ても分かる通り、このフィルタリング処理自体が不適切なのは否めませんが…リスクを少しでも減らそうという観点から見れば、このフラグは使用すべきではないと言えるでしょう。

```php:コード
$_POST['url'] = "java\xffscript:alert('XSS')"; // リクエストが来たと仮定
if (stripos($_POST['url'], 'javascript:') !== 0) {
    $url = htmlspecialchars($_POST['url'], ENT_HTML401 | ENT_IGNORE, 'UTF-8');
    echo '<a href="' . $url . '">URL</a>';
}
```

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

### `ENT_SUBSTITUTE` 指定

無効なシーケンスは同じバイト長の **`�` で構成される文字列に置換** されます。 `ENT_IGNORE` のようなリスクが発生したりすることはありません。 

```php:コード
$_POST['url'] = "java\xffscript:alert('XSS')"; // リクエストが来たと仮定
if (stripos($_POST['url'], 'javascript:') !== 0) {
    $url = htmlspecialchars($_POST['url'], ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8');
    echo '<a href="' . $url . '">URL</a>';
}
```

```html:実行結果
<a href="java�script:alert('XSS')">URL</a>
```

### 無指定

無効なシーケンスが検知された時点で返り値は **空文字列** になります。 `ENT_IGNORE` のようなリスクが発生したりすることはありません。 

```php:コード
$_POST['url'] = "java\xffscript:alert('XSS')"; // リクエストが来たと仮定
if (stripos($_POST['url'], 'javascript:') !== 0) {
    $url = htmlspecialchars($_POST['url'], ENT_HTML401, 'UTF-8');
    echo '<a href="' . $url . '">URL</a>';
}
```

```html:実行結果
<a href="">URL</a>
```

## <ins>文書タイプとして</ins>無効なシーケンスの扱い

セキュリティを考慮するのだけが目的であれば、あまりこの処理は重要ではないかもしれません。

### `ENT_DISALLOWED` 指定

無効なシーケンスは同じ長さの **`�` で構成される文字列に置換** されます。`ENT_IGNORE` のようなリスクが発生したりすることはありません。 

文書タイプとして有効かどうかは **[unicode_cp_is_allowed](https://github.com/php/php-src/blob/master/ext/standard/html.c#L705)** 関数の実装に従って判定されます。文書タイプによって差がある一例として、 **[非文字](http://ja.wikipedia.org/wiki/Unicode%E6%96%87%E5%AD%97%E3%81%AE%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0#.E9.9D.9E.E6.96.87.E5.AD.97)** と呼ばれる `﷐` を用いた際の結果を示します。HTML5では非文字の使用は許可されていません。

```php:コード
function test($string, $type) {
    return htmlspecialchars($string, ENT_DISALLOWED | constant($type), 'UTF-8');
}
foreach (['ENT_HTML401', 'ENT_HTML5'] as $type) {
    printf('% 11s: %s' . PHP_EOL, $type, test("\xef\xb7\x90", $type));
}
```

```txt:実行結果
ENT_HTML401: ﷐
  ENT_HTML5: �
```

### 無指定

何も影響を受けません。

```php:コード
function test($string, $type) {
    return htmlspecialchars($string, constant($type), 'UTF-8');
}
foreach (['ENT_HTML401', 'ENT_HTML5'] as $type) {
    printf('% 11s: %s' . PHP_EOL, $type, test("\xef\xb7\x90", $type));
}
```

```txt:実行結果
ENT_HTML401: ﷐
  ENT_HTML5: ﷐
```

# **まとめ**

**[htmlspecialchars](http://www.php.net/manual/ja/function.htmlspecialchars.php)** 関数のラッパー関数の実装例を紹介します。

```php:互換性を考慮したオーソドックスな実装
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
```

```php:不正なリクエストに対しても少し親切になった実装
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
```

```php:正しいXMLを表示することを強く意識した実装
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES | ENT_XML1 | ENT_DISALLOWED, 'UTF-8');
}
```

「不正なリクエスト」としているものに関しては…意図的に不正な文字コードが第三者によって渡される場合に限らず、不意に期待していない文字コードが渡されてしまうことも含まれます。例えば、<ins>Windows環境ではOSによって渡されるエラーメッセージが全て</ins> **`SJIS-win`** です。 **[PDO](http://www.php.net/manual/ja/book.pdo.php)** クラスによって自動的にスローされる例外や **[fsockopen](http://www.php.net/manual/ja/function.fsockopen.php)** 関数で発生するエラーのメッセージをキャプチャしたものがこれに該当する可能性があります。そういったときに **何も表示しない** のではなく **文字化けしていることを知らせてくれる** ようになる、という意味では有用な処理でしょう。