自分のWordPressで学ぶPHP講座(AIの変更を判定する)

区分3 差分を判断する★★★

No.07 入力と出力の安全

AIが出したPHPを判定する時、いちばん事故につながりやすいのが「外から来た値を、そのまま使っていないか」です。見る所は決まった数個の関数で、パターンで覚えられます。区分3の中心なので、しっかりやります。
目次
  1. 第0章 前提知識
  2. 第1章 入ってくる値を整える
  3. 第2章 画面に出す時の包み方
  4. 第3章 cookie を送る時の引数
  5. 第4章 動かしてはいけない場面と、キャッシュ
  6. 確認問題

この単元で扱うのは、次の5つです。

第0章 前提知識

① 外から来る値 :訪問者が自由に書き換えられる値

サイトに届く値のうち、PHPの側で決めたものではなく、アクセスした人が自由に作れるものがあります。 URLの末尾の ?look=on のような部分、フォームの送信内容、ブラウザが覚えている cookie(ブラウザに保存される小さなメモ)がその代表です。

PHPでは、これらが決まった名前の入れ物(スーパーグローバル変数)に入って届きます。

入れ物 何が入っているか
$_GET URLの ? 以降の値
$_POST フォームの送信内容
$_COOKIE ブラウザから送られてきた cookie

どれも、届いた値は、書いた人の意図どおりとは限りません。 窓口にいくらでも書類を出せる状況を想像してください。窓口の職員(PHP)は、出された書類を、確かめずに奥の部署に回してはいけません。

② XSS :入れた文字を、他の人のブラウザで動かされる攻撃

XSS(クロスサイトスクリプティング)は、外から来た値の中に、悪意のあるプログラム(<script> など)を紛れ込ませ、それがそのままページに出ることで、他の訪問者のブラウザで動いてしまう攻撃です。 一言でいえば、「入力欄に書かれた文字が、ページの一部として実行されてしまう」ことです。

防ぐ原則は2つだけです。 入ってくる時に整えること、出す時に無害な形に変えること。この単元の関数は、全部この2つのどちらかです。

③ 似た言葉との違い

何のことか
sanitize(サニタイズ) 入ってきた値を、使える形に整える。不要な文字を落とす
escape(エスケープ) 画面に出す時に、記号を無害な形に変える。< を &lt; にするなど
validate(検証) 値が許される範囲かを確かめる。「on か off のどちらか」など

「入る側はサニタイズ・検証、出る側はエスケープ」と、位置で分けて覚えます。

第1章 入ってくる値を整える

① 受け取る時の定番の形 必ず覚える

実物の子テーマには、URLの ?look=on を読んで cookie を立てる関数があります。まず、値を受け取る行だけを見ます。

function lk_look_preview_cookie() {
  if ( is_admin() || ! isset( $_GET['look'] ) || headers_sent() ) {
    return;
  }
  $v    = sanitize_key( wp_unslash( $_GET['look'] ) );
  ...
}

読み方は次のとおりです。

  1. isset( $_GET['look'] ):その値が届いているかを先に確かめる(No.04の isset)。無い時は何もしない
  2. wp_unslash( ... ):WordPressが値に自動で付けた余計なバックスラッシュ(\)を取り除く
  3. sanitize_key( ... ):英小文字・数字・ハイフン・アンダースコア以外を取り除く
  4. 整えた結果を $v に入れ、以降はこの $v だけを使う

2の理由は、WordPressが $_GET・$_POST・$_COOKIE の値に、あらかじめバックスラッシュを付けてから渡してくる仕様だからです。取り除いてから使うのが決まりです。 3の sanitize_key は、on や off のような短い合言葉を扱う時に使います。<script> のような記号は消えます。

外から来た値は、整える前の $_GET[...] を直接使わず、整えた変数を使う。AIの変更では、まずこの1点を探します。

② 範囲を決めて比べる 必ず覚える

整えたあとの使い方も見ておきます。

if ( 'on' === $v ) {
  ...cookie を立てる...
} elseif ( 'off' === $v ) {
  ...cookie を消す...
}

on と off のどちらかに完全に一致した時だけ動いて、それ以外の文字は何も起こしません。 このように、許す値をあらかじめ決めて比べる形は、いちばん安全です(三つのイコールは「型まで含めて完全に同じ」という比較、No.04の比較演算子です)。

逆に、危ないのは「届いた値を、そのまま別の処理に渡す」形です。 たとえば、届いた文字をそのままファイルの名前にする、データベースの検索に使う、といった使い方は、AIが出してきた時に立ち止まる所です。

③ 専用の関数の使い分け さらっと

sanitize_key のほかにも、WordPressには値の種類ごとに整える関数があります。見たら意味が分かる程度で構いません。

関数 整えるもの
sanitize_key 短い合言葉(英小文字・数字・- ・_)
sanitize_text_field 1行の文字(タグや余分な空白を取る)
absint 0以上の整数(ID・件数など)

詳しい一覧は、公式の「Sanitizing Data」にあります。

第2章 画面に出す時の包み方

① 出す場所で関数を使い分ける 必ず覚える

画面に値を出す時は、出す場所に合った関数で包みます。 実物のトップの帯を出す部分から、3つの形を拾います。

echo '<h2 class="lk-h1"><span>' . esc_html( $t1 ) . '</span></h2>';
echo '<p class="lk-lead">' . esc_html( $c['hero_lead'] ) . '</p>';
echo '<a class="' . esc_attr( $b['class'] ) . '" href="' . esc_url( lk_look_url( $b['url'] ) ) . '">'
   . esc_html( $b['label'] ) . '</a>';
関数 使う場所 何をするか
esc_html タグの間の文字 < > & などを、文字として表示する形に変える
esc_attr タグの属性の値(class="..." など) 引用符を閉じて属性を壊せないようにする
esc_url href や src のURL 危ない書き出し(javascript: など)を取り除く

3つ目の例で、1本のリンクの中に3種類が混ざっているのが分かります。 文字は esc_html、class は esc_attr、行き先は esc_url です。包む関数を取り違えると(たとえばURLを esc_html で包むと)、安全も見た目も中途半端になります。

一部だけ HTML のタグを許したい時は、wp_kses という関数が使われます。同じ帯に、こんな行があります。

echo '<p class="lk-bubble">' . wp_kses( $c['bubble'], array( 'mark' => array() ) ) . '</p>';

array( 'mark' => array() ) は、許すタグを <mark> だけにする指定です。許すタグの一覧が短いか、必要以上に広くないかを見ます。

② 「あなたが書いた文字」と「外から来た文字」 必ず覚える

ここで疑問が出るかもしれません。 上の例の $t1 や $c['hero_lead'] は、子テーマの中に自分で書いた文字列で、訪問者が書き換える値ではありません。それでも包んでいます。

これは、WordPressの公式な考え方に沿った書き方です。「どこから来た値かは考えず、出す時は必ず包む」という習慣にしておくと、後で値の出どころが変わった時にも事故が起きません。 AIの変更でも、echo で出している箇所に包む関数が付いているかを、全部の echo で見ます。

特に危ないのは、次のように外から来た値を直接出す形です。

echo $_GET['q'];

訪問者が ?q=<script>…</script> のようなURLを作って誰かに踏ませると、そのスクリプトが他の人のブラウザで動きます。 echo esc_html( $_GET['q'] ); なら、記号は文字として表示されるだけで、動きません。

① setcookie の7つの引数 必ず覚える

cookie は、サーバーがブラウザに「これを覚えておいて」と送る小さなメモです。 PHPでは setcookie という関数で送ります。引数(関数に渡す値)の数が多いので、順番ごとに読みます。実物は次の1行です。

setcookie( 'look_preview', '1', time() + 30 * DAY_IN_SECONDS, $path, COOKIE_DOMAIN, is_ssl(), true );
順 値 意味
1 'look_preview' cookie の名前
2 '1' 値
3 time() + 30 * DAY_IN_SECONDS 期限。いまから30日後
4 $path どのURLの範囲で送るか。$path は、その範囲を入れた変数
5 COOKIE_DOMAIN どのドメインに送るか。WordPressの設定で決まっている定数(途中で変わらない名前付きの値)
6 is_ssl() secure。https の時だけ送る。is_ssl() は、いま https の通信かどうかを返す関数
7 true httponly。JavaScriptから読めなくする

3番目の DAY_IN_SECONDS は、1日の秒数(86400秒)を表すWordPressの定数です。だから 30 * DAY_IN_SECONDS は30日になります。

4番目の「パス」と5番目の「ドメイン」は混ざりやすいので、例で分けます。 パスは、サイトの中のどのURLに送るかです。/ ならサイトのどのページにも、/line/ なら example.com/line/ の下のページだけに送ります。 ドメインは、どのサイト名に送るかです。www.example.com なら www のサイトだけ、.example.com なら blog.example.com のような別名のサイトにも送ります。

判定で見るのは、最後の2つです。 6番目の secure と7番目の httponly が、付いているか。secure が false だと、暗号化されていない通信にも cookie が流れます。httponly が false だと、XSS があった時に、スクリプトが cookie を盗み出せます。

cookie を消す時は、期限を過去にして送り直します。実物は setcookie( 'look_preview', '', time() - 3600, ... ) で、値を空に、期限を1時間前にしています。

② 送れる時と送れない時 さらっと

cookie は、ページの内容を出し始める前でないと送れません。 実物の関数が、冒頭の条件に headers_sent()(もう出力を始めたか)を入れているのは、そのためです。そして、この関数を init のフック(No.05)で動かしているのも、画面を出す前に送りたいからです。

AIの変更で、cookie を送る処理を、wp_head のような画面の途中のフックに移した時は、動かなくなる可能性があるので注意して見ます。

第4章 動かしてはいけない場面と、キャッシュ

① 管理画面・Ajax・REST を除く 必ず覚える

見た目を変える処理は、公開側の画面だけに効けばよく、管理画面や裏側の通信で動くと、思わぬ壊れ方をします。 実物のゲートの冒頭が、その除外です。

function lk_look_enabled() {
  static $on = null;
  if ( null !== $on ) {
    return $on;
  }
  if ( is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
    return $on = false;
  }
  ...
}

最初の2行 static $on = null; と if ( null !== $on ) は、この関数の答えを覚えておく仕組みです。 static $on = null; は、関数が終わっても消えない入れ物 $on を、最初は空(null)で用意する行です。null !== $on は「空ではない=もう答えを覚えているか」の確認で、覚えていればその答えを返し、2回目以降は判定をやり直しません。

その下の3つの条件が、||(または)でつながっています。

どれかに当てはまれば false を返して、何もしません。 return $on = false; は「$on に false を入れて、その値を返す」という1行です。

AIが画面の見た目を変える処理を足してきた時は、管理画面や REST でも動いてしまわないかを見ます。除外が無いと、投稿の編集画面が崩れたり、REST の返事に余計な物が混ざったりします。

② ページキャッシュとの関係 必ず覚える

多くのWordPressサイトは、表示を速くするために、できあがったHTMLを保存しておいて、次の人にも同じ物を配る仕組み(ページキャッシュ。WP Super Cache などのプラグイン)を使っています。 ここに、「特定の人にだけ出す」処理が重なると、事故が起きます。

下見のつもりで「cookie を持つ人にだけ新しいデザインを出す」処理を足したとして、その人のために作られたHTMLがキャッシュに保存されたら、cookie の無い人にもそのHTMLが配られてしまいます。 実物は、これを防ぐために、プレビュー中はキャッシュさせない指定を入れています。

if ( isset( $_GET['look'] ) || ( isset( $_COOKIE['look_preview'] ) && '1' === $_COOKIE['look_preview'] ) ) {
  if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); }
  nocache_headers();
}

DONOTCACHEPAGE は定数(途中で変わらない名前付きの値)で、キャッシュのプラグインに「このページは保存しないで」と伝える合図、nocache_headers() はブラウザや中継サーバーに「この返事を保存しないで」と伝える関数です。

人によって出し分ける変更には、キャッシュに保存させない指定があるかを確かめます。

○ よくある勘違い:「自分で書いた文字列だから包まなくていい」→ 出す時は必ず包む習慣にします(第2章②)
○ よくある勘違い:「sanitize したから、そのまま echo しても安全」→ 入る側と出る側は別。出す時にも必ず esc を付けます
○ 頼み方の例:「この変更で $_GET・$_POST・$_COOKIE を使っている行と、echo している行を全部挙げて。それぞれ整える関数・包む関数が付いているか表にして」
○ 公式の用語は Data Validation・Securing(出力のエスケープ)にまとまっています。出典を参照してください
○ 関係する単元:No.05(init などのフック)/No.06(差分の読み方)/No.11(5つの問い)

外から来た値は、整えてから使い、出す時は esc_html・esc_attr・esc_url で包む。cookie は secure と httponly を見る。管理画面と REST を除き、人で出し分ける処理はキャッシュに注意する。

確認問題

問1 AIが、検索ページに echo $_GET['keyword']; と書いた変更を出してきました。どう判定しますか?

問2 次の3つの出力のうち、使う関数が合っているものはどれでしょうか。

問3 AIが cookie の送信を setcookie( 'pv', '1', time() + 86400, '/' ); の形で出してきました。確認すべき点は?

問4 AIが「特定の人にだけ新しいデザインを出す処理」を、キャッシュ対策なしで足しました。キャッシュのプラグインが入っているサイトで起きうることは?

出典:WordPress公式 Theme Handbook「Data Validation」「Securing Output」「Sanitizing Data」(https://developer.wordpress.org/apis/security/)/esc_html(https://developer.wordpress.org/reference/functions/esc_html/)/esc_url/wp_unslash/sanitize_key の各リファレンス/wp_kses/nocache_headers の各リファレンス/PHP公式「setcookie」(https://www.php.net/manual/ja/function.setcookie.php)/PHP公式「定義済みの変数」(https://www.php.net/manual/ja/reserved.variables.php)/自分のサーバーの子テーマの functions.php。