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つのケースを考察してみましょう。

  1. $_GET 変数に "name" キーが存在しない場合
  2. $_GET 変数に "name" キーは存在するが値が空の場合
  3. $_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つのパターンを以下のように制御することができるようになりました。

  1. $_GET 変数に "name" キーが存在しない場合 => null を返す
  2. $_GET 変数に "name" キーは存在するが値が空の場合 => "" 空文字を返す
  3. $_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では NULLfalse といった値を文字列型にキャストするとすべて "" に置き換わるようになっています。つまりこのように実装することで、3つのパターンの入力を次のように変換できます。

  1. $_GET 変数に "name" キーが存在しない場合 => "" を返す
  2. $_GET 変数に "name" キーは存在するが値が空の場合 => "" 空文字を返す
  3. $_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 関数を使う