PHPでは、HTMLエスケープ用の関数としてhtmlspecialcharsが用意されています。
今日の日記では、htmlspecialcharsについて書きます。これと近い働きをするhtmlentitiesについても触れます。
htmlspecialcharsの基本
こんな感じで使います。
<?php echo htmlspecialchars($string, ENT_QUOTES, "UTF-8"); ?>
関数の引数は3つあります。
引数 | 省略 | 概要 |
---|---|---|
第一引数 | 不可 | エスケープ対象の文字列 |
第二引数 | 可 | クォート文字の扱い(後述) |
第三引数 | 可 | 文字コード(後述) |
第二引数は、以下の3つの値のいずれかを指定可能です。
値 | エスケープ対象文字 |
---|---|
ENT_NOQUOTES | < > & |
ENT_COMPAT | < > & " |
ENT_QUOTES | < > & " ' |
第二引数を指定しない場合のデフォルトは、ENT_COMPATです。
第三引数は、第一引数の文字列(バイト列)が、どの文字コードのバイト列であるかを指定します。デフォルトはISO-8859-1です。
htmlspecialcharsのポイント
まずは第二引数についてです。HTMLタグの属性値や属性名を「'」で囲い、その中にhtmlspecialcharsした文字列を挿入する場合、第二引数にENT_QUOTESを指定しなければなりません。
開発チーム内の規約などで、属性は「"」で囲うと決めていても、ついうっかりミスをすることもあるので、無条件にENT_QUOTESをつけておいた方がよいと思います。
次に第三引数です。指定できる文字コードには制限があります。日本語の文字を含むものでは、Shift_JIS、EUC-JP、UTF-8のみが指定可能です(eucJP-winやSJIS-winなどは使用できません)。
文字コードを指定する目的は、マルチバイト文字の2バイト目などに、0x3C(<) 0x3E(>) 0x26(&) 0x22(") 0x27(')のバイトが含まれている場合などに、これらを誤ってエスケープしないようにすることです。
しかし、日本語で用いられるShift_JIS、EUC-JP、UTF-8では、2バイト目以降にエスケープ対象の文字は含まれていません。そのため、意味的には第三引数に文字コードを指定するのが正しいですが、指定しなくても実のところ問題はありません。
ネット上には「不正な文字を利用したXSS攻撃*1を防ぐために、第三引数を指定する方がよい」というような記述もありましたが、これは日本語用の文字コードを使う限りは正確ではありません。文字コードを指定したところで、関数の出力結果は何ら変わりません。攻撃を防ぐ効果はないのです。
<?php $s = htmlspecialchars("\xA1", ENT_QUOTES, "EUC-JP"); echo bin2hex($s); // HEXダンプして出力 → a1
上記の例は、EUC-JPでの不正な文字(0xA1)が、htmlspecialcharsをすり抜けることを示しています。もし上記の$sをHTMLに出力すると、IEなどのブラウザでは後続の文字が破壊される現象が発生します。
htmlentitiesとの違い
htmlspecialcharsと似たような機能を持つ、htmlentitiesという関数があります。
しかし両者の違いは、余り良く理解されていないように思います。両者の違いは、htmlentitiesでは、U+00A0〜U+00FFに相当する文字*2をエスケープ(実体参照化)するが、htmlspecialcharsではエスケープしないということです。
htmlentitiesを使う場合、第三引数の文字コードの指定は必須です。指定しないと、ISO-8859-1とみなされて、0xA0〜0xFFまでのバイトがエスケープされます。これにより、日本語の文字の多くは破壊されてしまいます。例えば、SJISの「古」(0x8C 0xC3)という文字をエスケープすると、2バイト目の0xC3がÃになります。
htmlentitiesの詳細
以降では、第三引数に適切な文字コードを指定する前提で記述します。
さきほど、htmlentitiesでは、U+00A0〜U+00FF相当の文字をエスケープすると書きました。htmlspecialcharsよりもエスケープ対象の文字が多いので、htmlentitiesの方が安全なのか?というと、そういうことでもありません。
まず、U+00A0〜U+00FFの文字がどのようなものかは、wikipediaなどに書かれています。wikipediaのページの、NBSP〜ÿまでの文字が該当します。
Shift_JISやEUC-JPなどで考えると、そもそもU+00A0〜U+00FF相当の文字は含まれていません*3。そのため、正規なShift_JISやEUC-JP文字列をhtmlentitiesに与えた場合と、htmlspecialcharsに与えた場合で、出力結果が異なることはありません*4。
ただし、正規ではない(不正な)Shift_JISやEUC-JP文字列を与えた場合には、両者の出力結果は異なることがあります。htmlentitiesは、不正な部分はISO-8859-1とみなすようで、0xA0〜0xFFのバイトは実体参照に置き換えられます。つまり、htmlentitiesにより、不正な文字を使ったXSS攻撃を防げる場合があります。
<?php $s = htmlentities("\xA1", ENT_QUOTES, "EUC-JP"); echo $s; // ¡ が出力される
しかしこの例では、EUC-JPでの0xA1を¡(¡)とみなす根拠は(恐らく)ないわけで、この動作は言ってみれば「バグ」「明文化されていない仕様」みたいなものでしょう。つまりWebAP開発者は、この動作に頼るべきではないと思います。
また、置き換えられるのは、0xA0〜0xFFの範囲だけです。ブラウザ依存ですが、Shift_JISでは0x81〜0x9F、EUC-JPでは0x8E〜0x8Fなど、この範囲に含まれない不正な文字を使ったXSS攻撃は可能です。
<?php $s = htmlentities("\x81", ENT_QUOTES, "Shift_JIS"); echo bin2hex($s); // HEXダンプして出力 → 81
UTF-8の文字列をhtmlentitiesに与えた場合も、Shift_JISやEUC-JPの場合と同様です。詳細は省きますが、不正な文字を使ったXSS攻撃は可能です。
まとめ
日本語のWebサイトの開発者を前提にすると、以下のようなことが言えると思います。
- htmlentitiesを使うべき場面はまずない。
- htmlspecialcharsの第二引数には、ENT_QUOTESを指定した方が良い。
- htmlspecialcharsの第三引数は、今のところ指定するメリットは無い。
- 不正な文字を使ったXSS攻撃は、別途の対処が必要。
3について補足すると、意味的には指定したいところです(何の効果もありませんが)。将来的にPHP言語仕様やHTML仕様に変更が加えられた場合に、指定しておいた方が良い場面が出てくるかもしれません。
最後に、PHP6では内部文字列をユニコードで持つようになると聞いています。やっと他の言語(JavaやASPなど)と同じようになる訳です。