【2021/10/15 追記】
この記事は更新が停止されています。現在では筆者の思想が変化している面もありますので,過去の記事として参考程度にご覧ください。
予備知識
PHPはフォームから送信された値などをコード実行開始に自動的に変数として使えるようにしてくれる非常に便利なプログラミング言語です.しかし,それをそのまま用いるとエラーが発生したり,脆弱性になってしまったりするケースがたくさんあります.使う前には適当なチェック処理が必要です.
どういった変数が対象になるか
以下に挙げられた変数は,ユーザーが勝手に値や構造を書き換えたり,送信をそもそも行わずにアクセスしたりすることが可能な信用できない変数だと思ってください.例え,ラジオボタンで選択肢を限定していたり,隠し要素として埋め込んでいたりしたとしても,これに該当してしまいます.
$_GET
- アクセスされたURLの
?
以降のクエリーストリングに含まれる情報. -
method
属性の値がget
であるフォームから送られた情報.
$_POST
-
method
属性の値がpost
であるフォームから送られた情報.
$_COOKIE
- Webブラウザから送信されるクッキー.
$_REQUEST
-
$_GET
$_POST
$_COOKIE
の内容が合成されて作られたもの.
なお, $_SESSION
はサーバー側に保存される変数なので,このチェック対象にはなりません.チェックすべきはセッションの有効期限が切れていないか,つまり $_SESSION
が空になってしまっていないかどうかです.
session_start();
if (!$_SESSION) {
/* セッション有効期限切れに対応する処理 */
}
これらの特殊な変数に関する詳細は 「リクエストパラメータ・セッションに関するまとめ」 で詳しく解説しています.
PHPのエラーの種類
PHPには主に以下のようなエラーがあります. (これで全てではありません)
E_PARSE (別名: Parse Error, Syntax Error, 文法エラー)
コードが文法的に誤っているときに発生します.一切の処理を行いません.
E_ERROR (別名: Fatal Error, 致命的なエラー)
処理を継続することが不可能になってしまった場合に発生します.発生したところで処理を停止します.これが発生するリスクを持つコードを書いてはいけません.
E_WARNING (別名: Warning, 警告)
処理を継続することは可能ですが,想定外の問題が起こったときに発生します.これが発生するリスクを持つコードは可能な限り書かないでおくべきです.
E_NOTICE (別名: Notice, 通知)
処理を継続することは可能ですが,想定内の問題が起こったときに発生します.これは手を抜いて無視する人と丁寧に対応する人に二分されますが,対応の仕方を知らないまま手抜きをするようなプログラマになってはならないと私は考えるので,ここでは徹底的に対応することにします.
エラー発生による弊害
- 本番環境でエラーメッセージが表示されてしまうと,サーバーを攻撃しようと企む人にヒントを与えてしまう事になります.
- ページのデザインが崩れて不格好になります.
このため,エラーを表示させるのはデバッグ環境だけにすることが殆どです.但し,エラーを非表示にしたとしても…
- 処理速度が低下します.
- php.iniの設定によってはエラーログが溜まります.
- php.iniの error_reporting の設定によっては表示されないエラーもありますが,処理速度の低下は防げません.
-
エラー制御演算子
@
を使えば表示はされなくなりますが,処理速度の低下は防げません.またデバッグ環境で何らかのバグが発生してしまった状態でも,バグに関わるエラーメッセージが全て表示されなくなってしまいます.デバッグ作業がより難航することになるので,可能な限りこの演算子に頼ることは避けたほうが無難でしょう.
このようにさまざまなデメリットがあるので,可能な限りエラー発生を防ぐように書きましょう.
全てのエラーを表示
php.iniを以下のように編集すれば全てのエラーが表示されるようになります.
error_reporting = -1
display_errors = On
もしphp.iniの書き換えが出来ない場合,使用しているサーバーがApacheであれば以下のように .htaccess ファイルを設置することで対応できます.
php_value error_reporting -1
php_flag display_errors On
それも不可能な場合や一時的に有効にしたい場合は,スクリプトの先頭で以下のコードを実行すればそれ以降は全てのエラーが表示されるようになります.但し E_PARSE はコードを読み取る段階で発生するエラーなので,これには対応することが出来ません.
error_reporting(-1);
ini_set('display_errors', 'On');
比較演算子 ==
と ===
の比較
入門書などでは ==
が主体的に使われていることが多いですが,私は ===
に全て統一して使うことを推奨します.前者は 型の相互変換 を勝手に行います.PHP以外の言語出身者からすれば,これほど気持ち悪いものは無いでしょう.
htmlspecialchars 関数によるエスケープ処理
HTML中で特殊な意味を持つ文字列は必ずエスケープしなければなりません.ユーザーから入力された値に対してこの処理を怠ると クロスサイトスクリプティング に関する脆弱性が発生してしまいます.
この処理を行うタイミングは表示する直前にしましょう.それ以外の場所でこの処理を行うべきではありません.例えばデータベースやファイルに格納するときにあらかじめエスケープしてしまうのは,ソフトウェア構成上の誤りだと言えます.
$string
エスケープしたい文字列を渡します.
$flags
フラグを指定します.省略した場合のデフォルト値は ENT_COMPAT
となります.実際には ENT_HTML401
との論理和となりますが,この関数の挙動には影響を及ぼさないのでここでは触れないことにします.詳しくは 「htmlspecialchars関数やhtmlentities関数で使用されるフラグの検証」 を参照してください.
ENT_COMPAT でエスケープされる文字
<
>
&
"
属性値を全てダブルクオーテーションで括っているのであればこれで問題はありません.但し,エスケープすれば安全だと思って安易に属性値として埋め込むのは避けましょう.例えば以下のケースではリンクをクリックしたときにJavaScriptが実行されてしまいます.
// $_POST['url'] = "javascript:alert('XSS')"; とする
$url = htmlspecialchars($_POST['url']);
echo '<a href="' . $url . '">リンク</a>';
ENT_QUOTES でエスケープされる文字
<
>
&
'
"
これを指定しておくのが一番無難ですが,こちらも上記と同様に埋め込む対象となる属性によってはリスクが発生するので細心の注意を払ってください.
$encoding
エンコーディングを指定します. PHP5.4以降とPHP5.3以前でデフォルト値が異なり,前者では UTF-8
,後者では ISO-8859-1
となっています.前者であれば指定を省略することが可能ですが,バージョンによって挙動が異なってしまうのも問題なので,省略せずに指定することがマニュアルでも強く推奨されています.
ここまでは省略できない必須パラメータだと思ってください.
$double_encode
&
<
>
のように既にエスケープされている文字をもう一度エスケープして
&amp;
&lt;
&gt;
とするかどうかを論理値で指定します.デフォルトは true
です.
ラクに記述できる方法
表示するときに毎回
<p><?php echo htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); ?></p>
と書くのは非常に煩わしいので,
function h($str)
{
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
のような関数を作っておくと
<p><?php echo h($str); ?></p>
として簡潔に書くことが出来ます.ここから末尾のセミコロンは省略して
<p><?php echo h($str) ?></p>
とすることができ,更に echo短縮構文 を使えば何と
<p><?=h($str)?></p>
ここまで短く出来てしまいます.非常に便利な構文なのですが,これがデフォルトで使えるのは PHP5.4以降 のみで,それより古いバージョンの場合はphp.iniで short_open_tag
が有効化されている場合にしか使えません.もしphp.iniの書き換えが出来ない場合,使用しているサーバーがApacheであれば以下のように .htaccess ファイルを設置することで対応できます.
php_flag short_open_tag on
チェック処理の概要
isset
エラーの発生するケース
PHPでは,原則的に未定義の変数や配列インデックスの値を表示または取得しようとしたとき, E_NOTICE レベルのエラーが発生します.
- 変数が未定義
→ Notice: Undefined variable - 配列インデックスが未定義
→ Notice: Undefined index
つまり,手抜きコーディングでよくある以下のようなチェック処理は不適当です.
if ($_POST['email'] == '') {
$errors[] = 'Eメールアドレスが入力されていません';
}
実際にフォームから送信してそのページに遷移した場合にはエラーは発生しませんが,直接そのページにアクセスした場合には$_POST['email']
は未定義となるので,以下のエラーが発生してしまいます.
Notice: Undefined index: email
なお,この手抜きのケースでは ===
を用いることは出来ません. 未定義の値を E_NOTICE レベルのエラーを発生しながら強引に取得しようとしたときその値は NULL
と見なされるので, 型の相互変換 により NULL
を ""
として判定させなければ正しい動作をすることが出来ないからです.
エラーの発生を防ぐ方法
isset は,変数が 未定義でない かつ NULLでない ことをエラーを発生させずにチェックすることが出来ます.但し,外部からの入力を格納した変数が NULL
を取るケースは存在しないので,このような使い方をする上では単に未定義でないかどうかのチェックを行うものとして考えていただいて構いません.
if (!isset($_POST['email'])) {
$errors[] = 'Eメールアドレスが送信されていません';
} elseif ($_POST['email'] === '') {
$errors[] = 'Eメールアドレスが入力されていません';
}
「送信されていない」と「入力されていない」を区別する必要がない場合,以下のようにまとめてしまっても構いません.
if (!isset($_POST['email']) || $_POST['email'] === '') {
$errors[] = 'Eメールアドレスが入力されていません';
}
is_string 関数
is_array 関数
最初に紹介した isset によるチェックだけで自然な利用方法でも発生してしまう大半のエラーは防ぐことが出来ます.しかし,厳密に全てのエラーを潰すにはまだ不十分です.外部からの入力で受け取るものには 文字列 と 配列 の2種類があるからです.
例えば,こういうコードがあったとします.
if (isset($_GET['var'])) {
echo $_GET['var'];
echo htmlspecialchars($_GET['var'], ENT_QUOTES, 'UTF-8');
echo $items[$_GET['var']];
echo 1 + $_GET['var'];
}
一見これで十分なように思えますが,エラーを完璧に潰すにはまだ不十分です.
http://example.com/test.php?var[]=hoge
正確にURLエンコードすれば
http://example.com/test.php?var%5B%5D=hoge
のようなクエリを受け取った時に, $_GET['var']
は以下のような 配列 になってしまいます.
[0 => 'hoge']
故にこの例では上から順に
Notice: Array to string conversion
Warning: htmlspecialchars() expects parameter 1 to be string, array given
Warning: Illegal offset type
Fatal error: Unsupported operand types
のようなエラーが発生してしまいます.
- 配列が E_NOTICE レベルのエラーを発生しながら文字列に強引に変換されたとき,
"Array"
という扱いになる. - 入力値をそのままPHPの組み込み関数に渡している場合, E_WARNING レベルのエラーを発生してしまうケースが多い.
- 配列のオフセットにスカラー値または
NULL
以外を指定したとき, E_WARNING レベルのエラーが発生する. - 計算不可能なオペランド構成で
+
-
などの演算子を使用したとき, E_ERROR レベルのエラーが発生する.
こういった事態を防ぐためには,厳密に 文字列であるかどうか もしくは 配列でないかどうか をチェックする処理が必要です.先ほど述べたように,可能性は 文字列 と 配列 の2種類しかないので,以下のどちらのパターンを採用しても構いません.意味が分かりやすいと思ったほうを選んでください.
is_string($var)
!is_array($var)
!is_string($var)
is_array($var)
ここでは is_string 関数の返り値の否定を用います.
if (!isset($_POST['email'])) {
$errors[] = 'Eメールアドレスが送信されていません';
} elseif (!is_string($_POST['email'])) {
$errors[] = 'Eメールアドレスが不正送信されました';
} elseif ($_POST['email'] === '') {
$errors[] = 'Eメールアドレスが入力されていません';
}
「送信されていない」と「不正送信された」と「入力されていない」を区別する必要がない場合,以下のようにまとめてしまっても構いません.
if (!isset($_POST['email']) || !is_string($_POST['email']) || $_POST['email'] === '') {
$errors[] = 'Eメールアドレスが入力されていません';
}
配列の場合は各要素ごとのチェックも必要です.
if (!isset($_POST['params']) || !is_array($_POST['params'])) {
$errors[] = 'パラメータが送信されていません';
} else {
foreach ($params as $key => $value) {
if (!is_string($value)) {
$errors[] = "パラメータ{$key}が不正です";
}
}
}
empty
empty は,変数が 未定義である または 型の相互変換によりfalse
と等しいと見なされる値である ことをエラーを発生させずにチェックすることが出来ます.つまり以下のセットはそれぞれ同じ意味となります.
!isset($var) || $var == false
!isset($var) || !$var
empty($var)
isset($var) && $var == true
isset($var) && $var
!empty($var)
""
は false
と見なされる値の1つなので,最初の isset だけを用いた例はこのように書くことも出来ます.
if (empty($_POST['email'])) {
$errors[] = 'Eメールアドレスが入力されていません';
}
2番目に紹介した is_string 関数によるチェックも織り交ぜるなら以下のようになります.
if (empty($_POST['email']) || !is_string($_POST['email'])) {
$errors[] = 'Eメールアドレスが入力されていません';
}
但し,""
だけでなく "0"
も false
と見なされてしまい, 「入力したのに入力していないと言われた」 と利用者から文句が飛んでくるかもしれません.これぐらいの手抜きは場合によっては許されると思いますが,厳密な処理に拘る場合には避けるべきでしょう.著者はかなり気にします.
フィルタ関数の活用
ここからは
- スクリプトの先頭のほうで送信されてきた値をチェックする.有効なものであれば適当なローカル変数にその値を代入し, 未定義 もしくは 想定外の型 であれば
""
NULL
false
などを代入する. - そのローカル変数を用いて条件分岐を行い,適宜エラーに該当するかどうかをチェックする.
という処理フローを想定して「1.」の部分のコードを提示します.こちらの方が全体的な見通しが良くなり,入力フォームを再表示するときの利便性も向上するからです.
文字列のみを許可する
if (!isset($_POST['name'])) {
$name = null;
} elseif (!is_string($_POST['name'])) {
$name = false;
} else {
$name = $_POST['name'];
}
filter_input 関数を利用すれば,これと等価な処理をもっと美しく書くことが出来ます.
$name = filter_input(INPUT_POST, 'name');
文字列として送信されてきた場合のみ何か処理を行いたい場合は
$name = filter_input(INPUT_POST, 'name');
if (is_string($name)) {
/* 文字列として送信されてきた場合のみ実行したい処理 */
}
と書けば良いでしょう.
文字列を強制する
if (!isset($_POST['name']) || !is_string($_POST['name'])) {
$name = '';
} else {
$name = $_POST['name'];
}
分岐パターンと代入先が1種類しかないので, 三項演算子 を利用することが出来ます.
$name = isset($_POST['name']) && is_string($_POST['name']) ? $_POST['name'] : '';
$name = !isset($_POST['name']) || !is_string($_POST['name']) ? '' : $_POST['name'];
filter_input 関数と文字列へのキャストを利用すれば,これらと等価な処理をもっと美しく書くことが出来ます.
$name = (string)filter_input(INPUT_POST, 'name');
1次元配列を強制する
if (isset($_POST['items'])) {
$items = $_POST['items'];
if (!is_array($items)) {
$items = [$items];
}
foreach ($items as $key => $value) {
if (!is_string($value)) {
unset($items[$key]);
}
}
} else {
$items = [];
}
配列へのキャスト と array_filter 関数を利用すれば,これと等価な処理をもっと美しく書くことが出来ます.
$items = isset($_POST['items']) ? (array)$_POST['items'] : [];
$items = array_filter($items, 'is_string');
配列から文字列への強引なキャストでは E_NOTICE レベルのエラーが発生してしまいますが,文字列から配列へのキャストではエラーが発生しません.キー 0
に対する値を1つ持つ配列としてエラー無しにキャストされます.
指定したキーに限定して1次元配列を強制する
テキストの配列を対象とする場合
<form method="post" action="">
A: <input type="text" name="texts[a]"><br>
B: <input type="text" name="texts[b]"><br>
C: <input type="text" name="texts[c]"><br>
<input type="submit" value="送信">
</form>
テキストボックスは入力内容が無くても空文字列として送信されますが,不正なリクエストに備えるには結局冗長に書かざるを得ません.
foreach (['a', 'b', 'c'] as $i) {
$texts[$i] =
isset($_POST['text'][$i]) && is_string($_POST['text'][$i]) ?
$_POST['text'][$i] :
''
;
}
チェックボックスの配列を対象とする場合
<form method="post" action="">
<input type="checkbox" name="checks[a]" value="1">A<br>
<input type="checkbox" name="checks[b]" value="1">B<br>
<input type="checkbox" name="checks[c]" value="1">C<br>
<input type="submit" value="送信">
</form>
チェックボックスに関しては,チェックしていないものは送信されません.また,今回のように値を実際には必要とせず,単純にチェックされたかされなかったかのみを判定したい場合は
- チェックされたもの →
true
- チェックされなかったもの →
false
として存在させておいた方が何かと扱いやすいと思います.これを実現するには以下のようにします.
foreach (['a', 'b', 'c'] as $i) {
$checks[$i] = isset($_POST['checks'][$i]);
}
指定した値に限定して1次元配列を強制する
先ほどのチェックボックスの例に関して,今度はキーではなく値に着目し,想定されない値は含まないようにします.
<form method="post" action="">
<input type="checkbox" name="checks[]" value="a">A<br>
<input type="checkbox" name="checks[]" value="b">B<br>
<input type="checkbox" name="checks[]" value="c">C<br>
<input type="submit" value="送信">
</form>
array_intersect 関数で値に注目したときの共通項を計算します. 0
から始まる数字添え字配列にした方が綺麗なので,必要に応じて最後に array_values 関数を通しておきます.
$checks = isset($_POST['checks']) ? (array)$_POST['checks'] : [];
$checks = array_values(array_intersect($checks, ['a', 'b', 'c']));
更なるステップアップ
可変変数 の利用
filter_input 関数を利用するとかなりコードを短くできますが,それでも項目数だけ繰り返しコールする必要がありました.
$name = (string)filter_input(INPUT_POST, 'name');
$email = (string)filter_input(INPUT_POST, 'email');
$comment = (string)filter_input(INPUT_POST, 'comment');
これを 可変変数 という機能を利用することで,どれだけ項目が増えても1つ分の記述だけで済ませることが出来ます.但し,変数名 $v
と一致する 'v'
が配列内に存在すると正しい処理が行えなくなるので十分注意してください.
foreach (['name', 'email', 'comment'] as $v) {
$$v = (string)filter_input(INPUT_POST, $v);
}
もちろん,以下のように特定の配列やオブジェクトの中に格納することも可能です.お好みに応じて適当に使い分けてください.
foreach (['name', 'email', 'comment'] as $v) {
$p[$v] = (string)filter_input(INPUT_POST, $v);
}
$p = new stdClass;
foreach (['name', 'email', 'comment'] as $v) {
$p->$v = (string)filter_input(INPUT_POST, $v);
}
自分でフィルタリング用の関数を作る
複雑な配列をフォームから受け取る場合などには,上記の工夫だけでは間に合わないかもしれません.そういったときには自分でフィルタリング用の関数を作って処理をするのが適当です.以下に私が自作したとても便利な関数を紹介しておきます.
具体的な値に関するバリデーションを行う
ここまでは
- 未定義ではないか
- 想定外の型ではないか
という処理のみに着目してきましたが,実際には
- 未定義ではないか
- 想定外の型ではないか
- 誤った形式の値ではないか
という処理になることがほとんどでしょう.EメールアドレスやURLなどのチェックがこれに該当します.詳しくは以下のまとめにて紹介しています.