PHP Tips | 文字列処理:実務向け便利系 - JSON 安全エンコード

PHP PHP
スポンサーリンク

「JSON 安全エンコード」で何を守りたいのか

JSON 自体はただのデータ形式ですが、「どこに出すか」で話が一気に“セキュリティの話”になります。
特に危険なのは、JSON をそのまま HTML や JavaScript の中に埋め込むときです。

例えば、サーバー側でこういうコードを書いたとします。

$data = ['name' => '</script><script>alert(1)</script>'];
echo '<script>const DATA = ' . json_encode($data) . ';</script>';
PHP

json_encode() 自体は正しいのですが、値の中に </script> が入っていると、
ブラウザ側では「script タグが途中で閉じた」と解釈され、
その後ろの <script>alert(1)</script> が“本物のスクリプト”として実行されてしまう可能性があります。

「JSON を作る」だけでなく、「HTML/JS に埋め込んでも安全な形にする」——
これが「JSON 安全エンコード」の目的です。


json_encode の基本と「フラグ」の存在を知る

まずは素の json_encode

PHP で JSON を作る基本は json_encode() です。

$data = ['name' => '山田太郎', 'age' => 20];

$json = json_encode($data);

echo $json;
// {"name":"山田太郎","age":20}
PHP

配列や連想配列を渡すと、いい感じに JSON にしてくれます。
ここまではよく見る使い方ですが、「安全に出力する」ためには第 2 引数のフラグが重要になってきます。

HTML/JS に埋め込むときに効いてくるフラグ

json_encode() には、いくつかの「危険な文字をエスケープする」ためのフラグがあります。代表的なのが次の 4 つです。

  • JSON_HEX_TAG : <>\u003C \u003E にする
  • JSON_HEX_AMP : &\u0026 にする
  • JSON_HEX_APOS : '\u0027 にする
  • JSON_HEX_QUOT : "\u0022 にする

これらを組み合わせることで、「タグや属性を壊しにくい JSON 文字列」にできます。


実務で使える「JSON 安全エンコード」ユーティリティ

HTML の中に直接埋め込む用

一番よくあるのが、「サーバー側で作ったデータを <script> 内の変数に埋め込む」パターンです。

$data = [
    'name' => '</script><script>alert(1)</script>',
    'age'  => 20,
];
PHP

これをそのまま json_encode() すると危ないので、
安全フラグを全部盛りにしたユーティリティ関数を用意します。

/**
 * HTML/JS に埋め込むための「安全な JSON エンコード」
 */
function json_encode_safe_for_html(mixed $value): string
{
    $flags =
        JSON_HEX_TAG   | // < >
        JSON_HEX_AMP   | // &
        JSON_HEX_APOS  | // '
        JSON_HEX_QUOT  | // "
        JSON_UNESCAPED_UNICODE | // 日本語を \uXXXX にしない
        JSON_UNESCAPED_SLASHES;  // / をエスケープしない(読みやすさ用)

    $json = json_encode($value, $flags);

    if ($json === false) {
        // エラー時の扱いはプロジェクト方針に合わせて
        throw new RuntimeException('JSON encode failed: ' . json_last_error_msg());
    }

    return $json;
}
PHP

使い方の例です。

$data = ['name' => '</script><script>alert(1)</script>'];

echo '<script>const DATA = ' . json_encode_safe_for_html($data) . ';</script>';
PHP

JSON_HEX_TAG などのおかげで、</script>< >\u003C \u003E に変換され、
ブラウザは「タグの終わり」と認識できなくなります。
結果として、意図しないスクリプト実行を防ぎやすくなります。


API レスポンス用の「安全」との違い

API では「HEX 系フラグ」はいらないことが多い

REST API のレスポンスとして JSON を返すだけなら、
HTML に埋め込むわけではないので、JSON_HEX_* 系は必須ではありません。

むしろ、次のようなフラグを使うことが多いです。

  • JSON_UNESCAPED_UNICODE : 日本語をそのまま出す(\uXXXX にしない)
  • JSON_UNESCAPED_SLASHES : /\/ にしない
  • JSON_PRETTY_PRINT : 開発中だけ読みやすく整形

API 用のユーティリティは、こんな感じになります。

function json_encode_safe_for_api(mixed $value): string
{
    $flags =
        JSON_UNESCAPED_UNICODE |
        JSON_UNESCAPED_SLASHES;

    $json = json_encode($value, $flags);

    if ($json === false) {
        throw new RuntimeException('JSON encode failed: ' . json_last_error_msg());
    }

    return $json;
}
PHP

ここでの「安全」は、主に「文字化けしない」「パースエラーにならない」という意味での安全です。
XSS 対策としての「安全」は、HTML に埋め込むときにこそ意識するポイントです。


例題で違いを体感する

危なそうな文字を含むデータ

$data = [
    'html' => '<div onclick="alert(1)">クリック</div>',
    'js'   => '</script><script>alert("XSS")</script>',
];
PHP

これをそれぞれの関数でエンコードしてみます。

echo json_encode($data);
// {"html":"<div onclick=\"alert(1)\">クリック<\/div>","js":"<\/script><script>alert(\"XSS\")<\/script>"}

echo json_encode_safe_for_html($data);
// {"html":"\u003Cdiv onclick=\u0022alert(1)\u0022\u003Eクリック\u003C/div\u003E","js":"\u003C/script\u003E\u003Cscript\u003Ealert(\u0022XSS\u0022)\u003C/script\u003E"}

echo json_encode_safe_for_api($data);
// {"html":"<div onclick=\"alert(1)\">クリック</div>","js":"</script><script>alert(\"XSS\")</script>"}
PHP

API 用は「そのまま返す」ので、クライアント側(ブラウザやアプリ)が JSON としてパースしてからどう扱うかを決めます。
HTML 埋め込み用は、「タグや属性として解釈されないように」危険な文字を全部 \uXXXX に変換しているのが分かります。


まとめ:今日からの「JSON 安全エンコード」ユーティリティ

押さえておきたいポイントは、次の 3 つです。

JSON 自体はただのデータだが、「HTML/JS に埋め込むとき」に XSS の入り口になりやすい。
json_encode()JSON_HEX_TAG などのフラグで、タグや属性を壊しにくい形にできる。
API 用と HTML 埋め込み用で、「安全の意味」と使うフラグが少し違う。

実務ユーティリティとしては、少なくともこの 2 本を持っておくと安心です。

function json_encode_safe_for_html(mixed $value): string
{
    $flags =
        JSON_HEX_TAG |
        JSON_HEX_AMP |
        JSON_HEX_APOS |
        JSON_HEX_QUOT |
        JSON_UNESCAPED_UNICODE |
        JSON_UNESCAPED_SLASHES;

    $json = json_encode($value, $flags);

    if ($json === false) {
        throw new RuntimeException('JSON encode failed: ' . json_last_error_msg());
    }

    return $json;
}

function json_encode_safe_for_api(mixed $value): string
{
    $flags =
        JSON_UNESCAPED_UNICODE |
        JSON_UNESCAPED_SLASHES;

    $json = json_encode($value, $flags);

    if ($json === false) {
        throw new RuntimeException('JSON encode failed: ' . json_last_error_msg());
    }

    return $json;
}
PHP

もし、あなたのテンプレートのどこかで '<script>const DATA = ' . json_encode($data) . ';</script>' のようなコードが素のまま使われていたら、そこがこの「JSON 安全エンコード」に差し替えるべきポイントです。そこを一箇所直すだけで、XSS の入り口をかなり狭めることができます。

PHPPHP
スポンサーリンク
シェアする
@lifehackerをフォローする
スポンサーリンク
タイトルとURLをコピーしました