追記:最近のOWASPガイドの更新でJavaScript文字列はUnicodeエンコードで安全性を確保するよう変更されました。元々このブログでもUnicodeエスケープのまま利用するように書いています。他の言語のユーザーはUnicodeエスケープを利用しましょう。PHPもASCII領域の文字をUnicodeエスケープするようにした方が良いと思います。これは提案して実現するように努力します。
JSONはJavaScriptのオブジェクトや配列を表現する方式でRFC 4627で定義されています。メディアタイプはapplication/json、ファイル拡張子はjsonと定義されています。
PHPにJSON形式のデータに変換するjson_encode関数とjson_decode関数をサポートしています。
JSON関数がサポートされている話は簡単!となれば良いのですが、 いろいろ考慮しなければならない事があります。
TL;DR; PHPのjson_encode()を安全に利用する方法
json_encode()を利用する場合
$json = json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
と利用します。これでもまだ最適なエンコード方式とは言えませんが、デフォルトとして最低限必要なオプションが
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
です。
JSONデータの文字エンコーディングは基本UTF-8です。UTF-8文字データは”予めバリデーションしておく”必要があります。
JSONの仕様 RFC 4627
PHPのJSON関数を見る前にRFC 4627の定義を見てみましょう。セキュリティ的に必要なのは今回も文字リテラルの取り扱いです。
2.5. Strings
The representation of strings is similar to conventions used in the C family of programming languages. A string begins and ends with quotation marks. All Unicode characters may be placed within the quotation marks except for the characters that must be escaped: quotation mark, reverse solidus, and the control characters (U+0000 through U+001F).
Any character may be escaped. If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF), then it may be represented as a six-character sequence: a reverse solidus, followed by the lowercase letter u, followed by four hexadecimal digits that encode the character’s code point. The hexadecimal letters A though F can be upper or lowercase. So, for example, a string containing only a single reverse solidus character may be represented as “\u005C”.
Alternatively, there are two-character sequence escape representations of some popular characters. So, for example, a string containing only a single reverse solidus character may be represented more compactly as “\\”.
To escape an extended character that is not in the Basic Multilingual Plane, the character is represented as a twelve-character sequence, encoding the UTF-16 surrogate pair. So, for example, a string containing only the G clef character (U+1D11E) may be represented as “\uD834\uDD1E”.
string = quotation-mark *char quotation-mark
char = unescaped /
escape (
%x22 / ; ” quotation mark U+0022
%x5C / ; \ reverse solidus U+005C
%x2F / ; / solidus U+002F
%x62 / ; b backspace U+0008
%x66 / ; f form feed U+000C
%x6E / ; n line feed U+000A
%x72 / ; r carriage return U+000D
%x74 / ; t tab U+0009
%x75 4HEXDIG ) ; uXXXX U+XXXXescape = %x5C ; \
quotation-mark = %x22 ; “
unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
要するに文字リテラルは
- 文字リテラルは ” (ダブルクオート)で囲む
- Unicodeエスケープは全ての文字に対して行える
- Unicodeのベーシックプレーン以外は(0xFFFF以上)サロゲートペアを使う
- 一部の文字には
\
を利用したエスケープが可能 - エスケープ無しでもOKな文字は
unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
JavaScriptの文字リテラルのエスケープと比べると、JSONはJavaScriptだからJSONの仕様はJavaScriptと同じ!と思っていると間違える事に気付くと思います。
- 文字リテラルは ‘ (シングルクォート)で囲めない
\
を利用したエスケープが定義されているが、 ’ (シングルクォート)による文字リテラルはないので定義されていない- ECMASCript仕様書でエスケープ対象のセパレータ文字がエスケープ対象の文字になっていない
- HEXエスケープが無い
- エスケープ無しで利用できる文字が異なる
RFC 4627はECMAScriptの仕様を基に作成されていますが、ECMAScript 5.1とは異なる部分があります。これには注意が必要です。
JSONを利用する利点の1つは相互運用性です。例えば、PostgreSQLはJSON型のデータ型をサポートしています。PostgreSQLのJSON型はRFC 4627準拠です。相互運用性を最大にするには、JavaScriptだけで動けば良い、というエスケープではなくRFC 4427に準拠したエスケープ方法を利用する必要があります。
これだけで終わりかというと、まだ注意事項があります。
他のJSON仕様
RFC 6901 JSON Pointer、RFC 6902 JSON Patchという標準もあります。
RFC 6901 JSON Pointer April 2013
3. Syntax
A JSON Pointer is a Unicode string (see [RFC4627], Section 3) containing a sequence of zero or more reference tokens, each prefixed by a ‘/’ (%x2F) character.
Because the characters ‘~’ (%x7E) and ‘/’ (%x2F) have special meanings in JSON Pointer, ‘~’ needs to be encoded as ‘~0’ and ‘/’ needs to be encoded as ‘~1’ when these characters appear in a reference token.
The ABNF syntax of a JSON Pointer is:
json-pointer = *( “/” reference-token )
reference-token = *( unescaped / escaped )
unescaped = %x00-2E / %x30-7D / %x7F-10FFFF
; %x2F (‘/’) and %x7E (‘~’) are excluded from ‘unescaped’
escaped = “~” ( “0” / “1” )
; representing ‘~’ and ‘/’, respectively
JSON Pointerの標準では / , ~
が特別な意味を持っているので、それぞれ ~0, ~1
にエンコード(エスケープ)しなければならない、としています。このブログはJSON Pointerの解説ではないので説明しませんが、RFCにはこのような決まりもあると知っておくと何かの役に立つかも知れません。
JSONを出力する場合の注意点
JSONを出力する場合、JavaScriptとしてのみ評価されるコンテクストなのか、HTMLとして評価されてからJavaScriptとして評価されるコンテクストなのかを注意しなければなりません。JavaScriptとしてのみ評価される場合、RFC 4627とECMAScriptのエスケープ仕様に従ってエスケープ処理すれば安全です。
Webサーバーから出力する場合、クライアントがバグなどで誤ってHTMLと解釈しなければ、MIMEタイプをapplication/jsonまたはtext/javascriptに設定すればJavaScriptとしてのみ評価されます。
{
"object": {
"property": "<Tag> in JSON is safe?"
}
}
HTMLの中に埋め込んた場合、HTMLと評価されてからJavaScriptとして評価されます。
var myjson = '{
"object": {
"property": "<Tag> in JSON is safe?"
}
}';
HTMLに埋め込む場合にはHTMLパーサーに誤ってHTML要素の一部として解釈されないように、HTMLの特殊文字(< > " ' & /
)をエスケープしなければなりません。
HTMLの特殊文字として誤解釈させない方法には
- HTMLエンティティ化する
- HTMLパーサーでは解釈できない方法でエスケープする
の二種類の方法があります。1の方法はJavaScript/JSON専用の場合は動作しません。2の方法は汎用性があり、ユーザが使うコンテクストを考えなくても利用できます。1の方法が間違っている訳ではありませんが、2の方が優れていると思います。
PHPのjson_encode
JSONを出力する場合の注意点を理解した上で、PHPのJSONエスケープ関数であるjson_encodeを見てみましょう。
(PHP 5 >= 5.2.0, PECL json >= 1.2.0)
json_encode — 値を JSON 形式にして返す
説明
string json_encode ( mixed $value [, int $options = 0 [, int $depth = 512 ]] )
value を JSON 形式にした文字列を返します。
json_encode関数は$valueの文字列を$optionsに従い、最大512までネストしたJSONデータを作成します。$valueが不正である場合などにはfalseを返します。
$optionsに指定できるオプションは次の通りです。
JSON_HEX_TAG (integer)
すべての < および > をそれぞれ \u003C および \u003E に変換します。 この定数は PHP 5.3.0 以降で使用可能です。JSON_HEX_AMP (integer)
すべての & を \u0026 に変換します。 この定数は PHP 5.3.0 以降で使用可能です。JSON_HEX_APOS (integer)
すべての ‘ を \u0027 に変換します。 この定数は PHP 5.3.0 以降で使用可能です。JSON_HEX_QUOT (integer) この定数は PHP 5.3.0 以降で使用可能です。
すべての ” を \u0022 に変換します。 この定数は PHP 5.3.0 以降で使用可能です。JSON_FORCE_OBJECT (integer)
非連想配列を使用した場合に、配列ではなくオブジェクトを出力します。 出力を受け取る側がオブジェクトを期待しており、配列が空っぽである場合などに特に便利です。 この定数は PHP 5.3.0 以降で使用可能です。JSON_NUMERIC_CHECK (integer)
数値形式の文字列を数値としてエンコードします。 PHP 5.3.3 以降で使用可能です。JSON_BIGINT_AS_STRING (integer)
大きな整数値を、文字列型でエンコードします。 PHP 5.4.0 以降で使用可能です。JSON_PRETTY_PRINT (integer)
返される結果の書式を、スペースを使って整えます。 PHP 5.4.0 以降で使用可能です。JSON_UNESCAPED_SLASHES (integer)
/ をエスケープしません。 PHP 5.4.0 以降で使用可能です。JSON_UNESCAPED_UNICODE (integer)
マルチバイト Unicode 文字をそのままの形式で扱います (デフォルトでは \uXXXX にエスケープします)。 PHP 5.4.0 以降で使用可能です。
沢山のオプションがあります。
まずオプション無しでの動作を確認します。
<?php $val[1] = new StdClass; $val[1]->p1 = '日本語'; $val[1]->p2 = '& < > " \' \\ / ;'; $val[1]->p3 = "\0"; $val[1]->p4 = "\t"; $val[2] = [123, 'abc', 1.23, ['a'=>1, 'b'=>2, '<tag>'=>3]]; $val[3] = ['日本語'=>'あいうえお']; var_dump(json_encode($val, JSON_PRETTY_PRINT));
出力
string(192) "{"1":{"p1":"\u65e5\u672c\u8a9e","p2":"& lt; > \" ' \\ \/ ;","p3":"\u0000","p4":"\t"},"2":[123,"abc",1.23,{"a":1,"b":2,"<tag>":3}],"3":{"\u65e5\u672c\u8a9e":"\u3042\u3044\u3046\u3048\u304a"}}"
エスケープ処理され日本語の文字列はUnicodeエスケープされている事がわかます。しかし、よく見ると少しおかしな処理もあります。見やすくするために$optionにJSON_PRETTY_PRINT(整形出力)を設定し実行します。
string(363) "{
"1": {
"p1": "\u65e5\u672c\u8a9e",
"p2": "& < > \" ' \\ \/ ;",
"p3": "\u0000",
"p4": "\t"
},
"2": [
123,
"abc",
1.23,
{
"a": 1,
"b": 2,
"<tag>": 3
}
],
"3": {
"\u65e5\u672c\u8a9e": "\u3042\u3044\u3046\u3048\u304a"
}
}"
HTMLとして処理されるコンテクストの場合、 < > がエスケープ処理されていない点に注意が必要です。JSONデータがHTMLに埋め込まれたり、MIMEタイプがtext/htmlでブラウザに送信された場合、JavaScriptインジェクションが可能になります。これを防ぐには
JSON_HEX_TAG (integer)
すべての < および > をそれぞれ \u003C および \u003E に変換します。 この定数は PHP 5.3.0 以降で使用可能です。
が必要です。
&もそのままで出力されています。HTMLとして解釈されるコンテクストに出力した場合、HTMLエンティティと誤って処理される可能性があります。これを防ぐには
JSON_HEX_AMP (integer)
すべての & を \u0026 に変換します。 この定数は PHP 5.3.0 以降で使用可能です。
が必要です。
” は \ でエスケープ処理されているのでJSON的には安全ですが、” がHTML中に現れるとHTMLパーサーによって属性値の開始・終了と誤って解釈されるリスクがあります。’ はJSONではエスケープする必要がないですが ” と同じ理由でエスケープする方が安全です。” と ‘ をエスケープするには
JSON_HEX_APOS (integer)
すべての ‘ を \u0027 に変換します。 この定数は PHP 5.3.0 以降で使用可能です。JSON_HEX_QUOT (integer) この定数は PHP 5.3.0 以降で使用可能です。
すべての ” を \u0022 に変換します。 この定数は PHP 5.3.0 以降で使用可能です。
/ が \/ に変換されてしまう動作はJSONのRFC標準ですが動作しない処理系があります。動作しない処理系に対応する為にPHP 5.4.0からエスケープ処理を無効にするオプションが追加されています。
JSON_UNESCAPED_SLASHES (integer)
/ をエスケープしません。 PHP 5.4.0 以降で使用可能です。
json_encode関数の改良
HTMLコンテクストに出力する場合に必要なオプションが細かく分かれており、あまり出来の良い関数とは言えません。今後の課題としては
- / のエスケープにはUnicodeエスケープを利用する
- HTMLコンテクストで必要になるエスケープ処理を1つのオプションにまとめる
- HTMLコンテクスト用にJSONでエスケープを行っても、JSONとしては正しいのでHTMLコンテクスト用のエスケープ処理をデフォルトで有効にする
- 多少のリスクがあってもデータ量を減らすためJSONでUnicode文字コードをそのまま送れる文字コードは、エスケープ無しで送れるようにするオプションを作る
が考えられます。
json_encode関数を安全に使う
問題もあるjson_encode関数ですが、安全に利用するには
json_encode($val, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
とすれば一応、HTMLとして解釈されるコンテクストに出力しても安全に使える事が分かります。JSONを出力する場合、JavaScriptとしてのみ評価されるコンテクストなのか、HTMLとして評価されてからJavaScriptとして評価されるコンテクストなのかを注意しなければなりませんが、この出力であれば考慮しなくても安全です。もっと簡単(デフォルト)で安全な出力ができれば良いのですが、現在の仕様では4つもオプション指定が必要です。
上記のオプションを利用した場合のjson_encode関数の出力
string(397) "{
"1": {
"p1": "\u65e5\u672c\u8a9e",
"p2": "\u0026 lt; \u003E \u0022 \u0027 \\ \u0026 \/ ;",
"p3": "\u0000",
"p4": "\t"
},
"2": [
123,
"abc",
1.23,
{
"a": 1,
"b": 2,
"\u003Ctag\u003E": 3
}
],
"3": {
"\u65e5\u672c\u8a9e": "\u3042\u3044\u3046\u3048\u304a"
}
}"
「一応」としている理由は、/
はHTMLコンテクストではHTMLエスケープが推奨されている文字だからです。バグの無いHTMLパーサーであれば\
のエスケープでも問題ないはずです。(JSON場合、他のHTML特殊文字をエスケープしているので普通は / をエスケープしなくても大丈夫です)
安全性を優先する場合、JSON_UNESCAPE_UNICODEオプション
JSON_UNESCAPED_UNICODE (integer)
マルチバイト Unicode 文字をそのままの形式で扱います (デフォルトでは \uXXXX にエスケープします)。 PHP 5.4.0 以降で使用可能です。
は利用すべきです。Unicodeエスケープはデータ量が増えますが、最も安全なデータ受け渡し方法です。
JSON_UNESCAPED_SLASHESは送信先のシステムが動作しない場合には利用してください。それ以外には使わなくても構いません。
JSON_UNESCAPED_SLASHES (integer)
/ をエスケープしません。 PHP 5.4.0 以降で使用可能です。
まとめ
いろいろ細かい事を書きましたが。PHPのjson_encode関数でJSONデータを作る場合、
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
をオプションとして指定して使えばOKです。指定するオプションは4つもありますが、簡単ですね!
デフォルトで安全な動作になるようにPHPのコードを改造するのが一番良いのですが。いろいろやるべき事があって、なかなかPHPのソースコードを改修する時間が取れませんが課題として覚えておくようにします。時間を取れるようにする為には割の良い業務を是非弊社へ ^^; (今もっとも優先度を高くしているPHPの改良作業は「セッション管理の超高速化」です)
PHPのセキュリティ入門書に記載するコンテンツのレビューも兼ねてPHP Securityカテゴリでブログを書いています。コメント、感想は大歓迎です。
参考リンク:
- 「出力対策だけのセキュリティ設計」が誤りである理由(重要)
- PHPのHTMLエスケープ
- PHP7とjson_decodeとjson_encodeの困った仕様 – 数値型データの問題
- JavaScript: / の \ によるエスケープは禁止
- RailsのJavaScript文字列エスケープ
- JavaScript文字列のエスケープ
コメントは受け付けていません。