PHP - WEB - 13. filter_input関数
ここからはWebアプリケーションのセキュリティについて学習していきます。一般的なWebアプリケーションはWebブラウザによって操作するため同時に多数のユーザが利用できます。Webアプリケーションにアクセスするユーザの中には悪意のあるユーザも存在する可能性があります。Webアプリケーションに脆弱性があると機密情報が漏洩したり、データが不正に操作されたり、他にも利用者のなりすましなど、様々な被害が発生します。Webアプリケーションを運営する企業において、これらの被害が発生すると社会的な信用を大きく落とすことにもなりかねません。
ここではPHPでWebアプリケーションを開発する上での脆弱性を作らないように、いくつかの観点からアプリケーションのセキュリティについて見直していきます。
PHPプログラムの開発(入力データの検証)
Webアプリケーションでは入力フォームを利用してユーザからの入力を処理することが頻繁にあります。そのためユーザの入力が妥当なものか検証するために入力チェックを実装することが必要になります。ここでは以前に作成した検索画面( search.html
)と検索ボタンをクリックしたときの検索処理( search.php
)を例に、PHPにおける入力チェックについて考察します。
それでは順にプログラムを確認していきましょう。まずは検索画面( search.html
)です。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PHP Sample</title>
</head>
<body>
<h3>Search</h3>
<hr>
<form action="search.php" method="get">
<input type="text" name="name">
<input type="submit" value="search">
</form>
<a href="search.php?name=a">Search: a</>
</body>
</html>
このプログラムは以前に作成したものと変わりはありません。画面にはGETリクエストを送信する入力フォームがあり、名前を入力するテキストボックスと検索ボタンを表示します。
続いて検索処理( search.php
)です。入力データのやりとりを確認するためにプログラムの先頭部分にプログラムを追加します。
<?php
$name = $_GET["name"];
var_dump($name);
die("debug");
$names = file("names.txt", FILE_IGNORE_NEW_LINES);
$searched_names = [];
if ($name !== "") {
for ($i = 0; $i < count($names); $i++) {
if (strpos($names[$i], $name) !== false) {
$searched_names[] = $names[$i];
}
}
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PHP Sample</title>
</head>
<body>
<h3>Search</h3>
<hr>
<ul>
<?php
for ($i = 0; $i < count($searched_names); $i++) {
?>
<li><?php echo $searched_names[$i]; ?></li>
<?php
}
?>
</ul>
</body>
</html>
ここではプログラムの修正部分を確認しておきましょう。
<?php
$name = $_GET["name"];
var_dump($name);
die("debug");
このプログラムでは先頭部分で $_GET["name"]
の値を $name
変数に代入しています。今回はユーザの入力データについて詳しく検証するために var_dump
関数を使って実際にユーザの入力を画面に出力するように修正しています。またその後に die
関数を呼び出すことでプログラムを終了させるようにしています。このように処理の途中で die
関数を呼び出すと、以降の処理は実行されないようになります。
プログラムの修正については以上です。まずはこれまでのプログラムの動作確認のためにビルトインWebサーバを起動しましょう。
$ php -S localhost:8000
続いてWebブラウザから検索画面( search.html
)にアクセスします。アドレスバーに以下のURLを入力します。
http://localhost:8000/search.html
まずは正常な入力パターンとしてテキストボックスに A
という文字を入力して検索ボタンをクリックしてみましょう。
そうすると上記のように var_dump
関数の出力によって $name
には "A"
という文字列( string
)データが代入されていることがわかります。またこのとき、アドレスバーに表示されるURLが次のようになっている点を確認していきましょう。
http://localhost:8000/search.php?name=A
GETリクエストで送信された入力フォームの値はこのようにURLの一部(クエリパラメータ)として表示されるようになります。
ここまでで修正したプログラムの動作確認ができたので、次にユーザの不正な入力パターンとして次の3つのケースを考察してみましょう。
$_GET
変数に"name"
キーが存在しない場合$_GET
変数に"name"
キーは存在するが値が空の場合$_GET
変数に"name"
キーが配列形式の場合
1. $_GET 変数に "name" キーが存在しない場合
まずは「 $_GET
に "name"
キーが存在しない」というケースです。このような状態を実現するには検索画面のテキストボックスを空にするのではなく、アドレスバーから検索処理のURLを直接指定するようにします。
http://localhost:8000/search.php
そうすると画面には次のようなメッセージが表示されるでしょう。
このメッセージはPHPの中でNotice(注意)に分類されるものですので、致命的な状況ではありませんが、このような想定外のケースをそのままにしておくのはあまり好ましくありません。
php.ini
ファイルの設定によってはNoticeメッセージが表示されないこともあります。その場合はphp.ini
ファイルのdiplay_errors
項目やerror_reporting
項目の設定を確認してください。
2. $_GET 変数に "name" キーは存在するが値が空の場合
次に「 $_GET
に "name"
キーは存在するが値が空」というケースです。このような状態を実現するには検索画面のテキストボックスを空の状態で検索ボタンをクリックするか、アドレスバーから以下のURLに直接指定するようにします。
http://localhost:8000/search.php?name=
実際にアクセスすると画面には次のような結果が表示されるでしょう。
var_dump
関数によって空文字が出力されているのがわかります。このようなケースは想定外というわけではありませんが、クエリパラメータにキーのみが存在する(値が存在しない)場合は $_GET
から取得した値は ""
空文字になると理解しておくようにしましょう。
3. $_GET 変数に "name" キーが配列形式の場合
さいごに3つ目の「 $_GET
に "name"
キーが配列形式」というケースです。これはイメージしにくいかもしれませんが、アドレスバーから以下のようにURLを指定することでクエリパラメータを配列で指定できます。
http://localhost:8000/search.php?name[]=A&name[]=B
実際にアクセスすると画面には次のような結果が表示されるでしょう。
var_dump
関数によって $name
の内容を出力していますが、配列データ( array
)として取得できていることがわかります。
このような仕組みは同名の複数のチェックボックスの入力を配列で受け取る、といったシーンで利用します。
ここまで3つのイレギュラーな入力パターンを確認してきましたが、いずれもユーザの操作次第で発生する可能性があります。そのためWebアプリケーションを開発する場合には、このような入力パターンも想定してプログラムを記述しておく必要があります。
入力チェックの実装
それではさきほどの3つの入力パターンを想定して入力チェックを実装してみましょう。検索処理( search.php
)を以下のように修正します。
<?php
$name = null;
if (!isset($_GET["name"])) {
$name = null;
} else if(!is_string($_GET["name"])) {
$name = false;
} else {
$name = $_GET["name"];
}
var_dump($name);
die("debug");
// ...省略
ここでは if
文を追加して $_GET["name"]
の内容を検証しています。 if
文の条件式を順に見ていきましょう。
if (!isset($_GET["name"])) {
まず1つ目の条件式では isset
関数を使って $_GET
変数に "name"
キーが存在しない場合を処理しています。これは1つ目の入力パターンで示した以下のアクセスを想定してのものです。
http://localhost:8000/search.php
今回の修正によって、もし上記のようにアクセスされた場合は $name
変数に null
を代入するようにしています。
次に2つ目の条件式です。
} else if(!is_string($_GET["name"])) {
is_string
関数は引数が文字列データの場合に true
、そうでない場合に false
を返します。ここでは !
否定を使って is_string
関数を呼び出しているので、引数の $_GET["name"]
が文字列型でない場合を条件にしています。これは3つ目のパターンで紹介したようなクエリパラメータに配列データを指定されたようなケースです。
http://localhost:8000/search.php?name[]=A&name[]=B
今回の修正によって、もし上記のようにアクセスされた場合は $name
変数に false
を代入するようにしています。
このような2つの条件をパスした場合は最後の else
ブロックによって $name = $_GET["name"]);
が実行されます。
今回の修正によって、さきほどの3つの入力パターンがどのように処理されるか改めて確認しておきましょう。
1. $_GET 変数に "name" キーが存在しない場合 - 入力チェック実装後
アドレスバーから以下のURLにアクセスします。
http://localhost:8000/search.php
そうすると画面には次のような結果が表示されるでしょう。
以前はNoticeメッセージが表示されていましたが、今回の修正で NULL
と表示されているのがわかります。
2. $_GET 変数に "name" キーは存在するが値が空の場合 - 入力チェック実装後
アドレスバーから以下のURLにアクセスします。
http://localhost:8000/search.php?name=
そうすると画面には次のような結果が表示されるでしょう。
前回と変わらず var_dump
関数によって空文字 ""
が出力されているのがわかります。
3. $_GET 変数に "name" キーが配列形式の場合 - 入力チェック実装後
アドレスバーから以下のURLにアクセスします。
http://localhost:8000/search.php?name[]=A&name[]=B
そうすると画面には次のような結果が表示されるでしょう。
以前は配列データが出力されていましたが、今回の修正で false
と表示されているのがわかります。
ここまでの修正で3つのパターンを以下のように制御することができるようになりました。
$_GET
変数に"name"
キーが存在しない場合 =>null
を返す$_GET
変数に"name"
キーは存在するが値が空の場合 =>""
空文字を返す$_GET
変数に"name"
キーが配列形式の場合 =>false
を返す
これまでに検索処理( search.php
)において if
文を使って複雑な変換処理を実装してきました。
<?php
$name = null;
if (!isset($_GET["name"])) {
$name = null;
} else if(!is_string($_GET["name"])) {
$name = false;
} else {
$name = $_GET["name"];
}
// ...省略
Webブラウザの入力フォームから送信される項目の一つひとつにこのような入力チェックを実装するのは手間がかかるので、このような変換処理は関数として定義しておくと便利です。実はPHPの組み込み関数の中には上記のような変換処理を行う filter_input
関数が用意されています。
filter_input 関数
それでは先程の検索処理( search.php
)を filter_input
関数を使うように修正してみましょう。
<?php
$name = filter_input(INPUT_GET, "name");
var_dump($name);
die("debug");
// ...省略
filter_input
関数は、第1引数にGETリクエストを処理する場合は INPUT_GET
定数、POSTリクエストを処理する場合は INPUT_POST
定数を指定します。また第2引数にはリクエストパラメータのキーを指定します。このように filter_input
関数を呼び出すだけで、さきほどの if
文と同じような変換処理が可能となります。
それでは実際に3つのパターンを検証してみましょう。
1. $_GET 変数に "name" キーが存在しない場合 - filter_input 関数
アドレスバーから以下のURLにアクセスします。
http://localhost:8000/search.php
そうすると画面には次のような結果が表示されるでしょう。
filter_input
関数による変換結果として NULL
が表示されているのがわかります。
2. $_GET 変数に "name" キーは存在するが値が空の場合 - filter_input 関数
アドレスバーから以下のURLにアクセスします。
http://localhost:8000/search.php?name=
そうすると画面には次のような結果が表示されるでしょう。
filter_input
関数による変換結果として空文字 ""
が表示されているのがわかります。
3. $_GET 変数に "name" キーが配列形式の場合 - filter_input 関数
アドレスバーから以下のURLにアクセスします。
http://localhost:8000/search.php?name[]=A&name[]=B
そうすると画面には次のような結果が表示されるでしょう。
filter_input
関数による変換結果として false
が表示されているのがわかります。
また filter_input
関数の呼び出しに、期待するデータ型へのキャストを組み合わせることでさらに実用的になります。
<?php
$name = (string)filter_input(INPUT_GET, "name");
var_dump($name);
die("debug");
// ...省略
ここでは filter_input
関数の呼び出し結果を文字列型( string
型)にキャストしています。PHPでは NULL
や false
といった値を文字列型にキャストするとすべて ""
に置き換わるようになっています。つまりこのように実装することで、3つのパターンの入力を次のように変換できます。
$_GET
変数に"name"
キーが存在しない場合 =>""
を返す$_GET
変数に"name"
キーは存在するが値が空の場合 =>""
空文字を返す$_GET
変数に"name"
キーが配列形式の場合 =>""
を返す
filter_input
関数の戻り値を string
型にキャストすることで、想定外のケースをすべて空文字として判断できるようになりました。
さいごに検索処理( search.php
)から不要な var_dump
関数、 die
関数を削除しておきましょう。
<?php
$name = (string)filter_input(INPUT_GET, "name");
$names = file("names.txt", FILE_IGNORE_NEW_LINES);
$searched_names = [];
if ($name !== "") {
for ($i = 0; $i < count($names); $i++) {
if (strpos($names[$i], $name) !== false) {
$searched_names[] = $names[$i];
}
}
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PHP Sample</title>
</head>
<body>
<h3>Search</h3>
<hr>
<ul>
<?php
for ($i = 0; $i < count($searched_names); $i++) {
?>
<li><?php echo $searched_names[$i]; ?></li>
<?php
}
?>
</ul>
</body>
</html>
以上でプログラムの修正は完了です。以降はWebブラウザから3つの入力パターンを試すといずれも次のような結果を返すようになります。
さらに高度な用法として
filter_input
関数の第3引数にはフィルターを指定できます。たとえばFILTER_SANITIZE_STRING
と指定するとタグの入力などを除去できます。詳細についてはPHPマニュアルを確認してみましょう。https://www.php.net/manual/ja/function.filter-input.php
まとめ
- Webアプリケーションのユーザの中には悪意のあるユーザも存在する
- リクエストパラメータは簡単にカスタマイズできるため、期待するデータが存在するか検証する必要がある
- リクエストパラメータの検証には
filter_input
関数を使う