Go to list of users who liked
Share on X(Twitter)
Share on Facebook
More than 3 years have passed since last update.
【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=-1display_errors=Onもしphp.iniの書き換えが出来ない場合,使用しているサーバーがApacheであれば以下のように.htaccess ファイルを設置することで対応できます.
php_value error_reporting -1php_flag display_errorsOnそれも不可能な場合や一時的に有効にしたい場合は,スクリプトの先頭で以下のコードを実行すればそれ以降は全てのエラーが表示されるようになります.但しE_PARSE はコードを読み取る段階で発生するエラーなので,これには対応することが出来ません.
error_reporting(-1);ini_set('display_errors','On');比較演算子== と=== の比較
入門書などでは== が主体的に使われていることが多いですが,私は=== に全て統一して使うことを推奨します.前者は型の相互変換 を勝手に行います.PHP以外の言語出身者からすれば,これほど気持ち悪いものは無いでしょう.
htmlspecialchars 関数によるエスケープ処理
HTML中で特殊な意味を持つ文字列は必ずエスケープしなければなりません.ユーザーから入力された値に対してこの処理を怠るとクロスサイトスクリプティング に関する脆弱性が発生してしまいます.
この処理を行うタイミングは表示する直前にしましょう.それ以外の場所でこの処理を行うべきではありません.例えばデータベースやファイルに格納するときにあらかじめエスケープしてしまうのは,ソフトウェア構成上の誤りだと言えます.
$string
エスケープしたい文字列を渡します.
$flags
フラグを指定します.省略した場合のデフォルト値はENT_COMPAT となります.実際にはENT_HTML401 との論理和となりますが,この関数の挙動には影響を及ぼさないのでここでは触れないことにします.詳しくは「htmlspecialchars関数やhtmlentities関数で使用されるフラグの検証」 を参照してください.
<>&"
属性値を全てダブルクオーテーションで括っているのであればこれで問題はありません.但し,エスケープすれば安全だと思って安易に属性値として埋め込むのは避けましょう.例えば以下のケースではリンクをクリックしたときにJavaScriptが実行されてしまいます.
// $_POST['url'] = "#"; とする$url=htmlspecialchars($_POST['url']);echo'<a href="'.$url.'">リンク</a>';<>&'"
これを指定しておくのが一番無難ですが,こちらも上記と同様に埋め込む対象となる属性によってはリスクが発生するので細心の注意を払ってください.
$encoding
エンコーディングを指定します. PHP5.4以降とPHP5.3以前でデフォルト値が異なり,前者ではUTF-8 ,後者ではISO-8859-1 となっています.前者であれば指定を省略することが可能ですが,バージョンによって挙動が異なってしまうのも問題なので,省略せずに指定することがマニュアルでも強く推奨されています.
ここまでは省略できない必須パラメータだと思ってください.
$double_encode
&<>
のように既にエスケープされている文字をもう一度エスケープして
&amp;&lt;&gt;
とするかどうかを論理値で指定します.デフォルトはtrue です.
ラクに記述できる方法
表示するときに毎回
<p><?phpechohtmlspecialchars($str,ENT_QUOTES,'UTF-8');?></p>と書くのは非常に煩わしいので,
functionh($str){returnhtmlspecialchars($str,ENT_QUOTES,'UTF-8');}のような関数を作っておくと
<p><?phpechoh($str);?></p>として簡潔に書くことが出来ます.ここから末尾のセミコロンは省略して
<p><?phpechoh($str)?></p>とすることができ,更にecho短縮構文 を使えば何と
<p><?=h($str)?></p>ここまで短く出来てしまいます.非常に便利な構文なのですが,これがデフォルトで使えるのはPHP5.4以降 のみで,それより古いバージョンの場合はphp.iniでshort_open_tag が有効化されている場合にしか使えません.もしphp.iniの書き換えが出来ない場合,使用しているサーバーがApacheであれば以下のように.htaccess ファイルを設置することで対応できます.
php_flag short_open_tagonチェック処理の概要
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'];echohtmlspecialchars($_GET['var'],ENT_QUOTES,'UTF-8');echo$items[$_GET['var']];echo1+$_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 conversionWarning: htmlspecialchars() expects parameter 1 to be string, array givenWarning: Illegal offset typeFatal 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($paramsas$key=>$value){if(!is_string($value)){$errors[]="パラメータ{$key}が不正です";}}}empty
empty は,変数が未定義である または型の相互変換によりfalseと等しいと見なされる値である ことをエラーを発生させずにチェックすることが出来ます.つまり以下のセットはそれぞれ同じ意味となります.
!isset($var)||$var==false!isset($var)||!$varempty($var)isset($var)&&$var==trueisset($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 と見なされてしまい,「入力したのに入力していないと言われた」 と利用者から文句が飛んでくるかもしれません.これぐらいの手抜きは場合によっては許されると思いますが,厳密な処理に拘る場合には避けるべきでしょう.著者はかなり気にします.
フィルタ関数の活用
ここからは
- スクリプトの先頭のほうで送信されてきた値をチェックする.有効なものであれば適当なローカル変数にその値を代入し,未定義 もしくは想定外の型 であれば
""NULLfalseなどを代入する. - そのローカル変数を用いて条件分岐を行い,適宜エラーに該当するかどうかをチェックする.
という処理フローを想定して「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($itemsas$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次元配列を強制する
テキストの配列を対象とする場合
<formmethod="post"action="">A:<inputtype="text"name="texts[a]"><br>B:<inputtype="text"name="texts[b]"><br>C:<inputtype="text"name="texts[c]"><br><inputtype="submit"value="送信"></form>テキストボックスは入力内容が無くても空文字列として送信されますが,不正なリクエストに備えるには結局冗長に書かざるを得ません.
foreach(['a','b','c']as$i){$texts[$i]=isset($_POST['text'][$i])&&is_string($_POST['text'][$i])?$_POST['text'][$i]:'';}チェックボックスの配列を対象とする場合
<formmethod="post"action=""><inputtype="checkbox"name="checks[a]"value="1">A<br><inputtype="checkbox"name="checks[b]"value="1">B<br><inputtype="checkbox"name="checks[c]"value="1">C<br><inputtype="submit"value="送信"></form>チェックボックスに関しては,チェックしていないものは送信されません.また,今回のように値を実際には必要とせず,単純にチェックされたかされなかったかのみを判定したい場合は
- チェックされたもの →
true - チェックされなかったもの →
false
として存在させておいた方が何かと扱いやすいと思います.これを実現するには以下のようにします.
foreach(['a','b','c']as$i){$checks[$i]=isset($_POST['checks'][$i]);}指定した値に限定して1次元配列を強制する
先ほどのチェックボックスの例に関して,今度はキーではなく値に着目し,想定されない値は含まないようにします.
<formmethod="post"action=""><inputtype="checkbox"name="checks[]"value="a">A<br><inputtype="checkbox"name="checks[]"value="b">B<br><inputtype="checkbox"name="checks[]"value="c">C<br><inputtype="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=newstdClass;foreach(['name','email','comment']as$v){$p->$v=(string)filter_input(INPUT_POST,$v);}自分でフィルタリング用の関数を作る
複雑な配列をフォームから受け取る場合などには,上記の工夫だけでは間に合わないかもしれません.そういったときには自分でフィルタリング用の関数を作って処理をするのが適当です.以下に私が自作したとても便利な関数を紹介しておきます.
具体的な値に関するバリデーションを行う
ここまでは
- 未定義ではないか
- 想定外の型ではないか
という処理のみに着目してきましたが,実際には
- 未定義ではないか
- 想定外の型ではないか
- 誤った形式の値ではないか
という処理になることがほとんどでしょう.EメールアドレスやURLなどのチェックがこれに該当します.詳しくは以下のまとめにて紹介しています.
推薦記事
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme