プログラミング

プログラミング初心者のPHP修羅の道#17〜PHP8上級問題を徹底追求する〜

PHP8上級資格取得へ向けたアウトプット記事になります。

PHP試験運営団体より公開されている模擬試験を1つ1つみていく形で事細かくみていきますが、勉強途中ゆえ間違っている部分に関してはご了承ください。

PHP上級資格のメイン教材はPHPマニュアルと指定されていますので、このマニュアルを主に踏襲した内容となっています。

主な勉強教材は以下になります。

1.独習PHP第4版

2.はじめてのPHP

 

3.PHP公式マニュアル

 

PHP8上級試験模擬問題

試験問題17

 

XSS (クロスサイトスクリプティング) に関する説明の中で、誤っているものを1つ選びなさい。
なお、すべてのコードの先頭には下記のコードが書かれているものとする。

declare(strict_types=1);
error_reporting(-1);

下記はマニュアルから一部引用した内容である。

htmlspecialchars ( string $string , int $flags = ENT_COMPAT , string|null $encoding = null , bool $double_encode = true ) : string
htmlentities ( string $string , int $flags = ENT_COMPAT , string|null $encoding = null , bool $double_encode = true ) : string

第二引数 (flags) の定数
ENT_COMPAT ダブルクオートは変換しますがシングルクオートは変換しません。
ENT_QUOTES シングルクオートとダブルクオートを共に変換します。
ENT_NOQUOTES シングルクオートとダブルクオートは共に変換されません。

 

 

選択肢①

 

HTMLの動的な生成において、実装に問題があるとXSS脆弱性が発生する。
そのためには、適切な引数で htmlspecialchars()関数、または htmlentities()関数を使う必要がある。
そのため、以下のコード

$input = "alert('test');";
$e_input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
echo $e_input;

を実行すると、

alert('test');

となり、必要なエスケープが全てなされているので、XSSを防ぐ事ができる。

 

 

選択肢のコードを実行すると実際には以下のように出力されます。

alert('test');

変換される文字については以下のようになっています。

引数については以下のようになっています。

htmlspecialchars($string, $flags, $encoding, $double_encode);

// $string     :エスケープ対象の文字列
// $flags      :エスケープの種類
// $encoding    :使用する文字エンコーディング
// $double_encode :HTMlエンティティを二重にエスケープするか

よって、この選択肢は誤りです。

 

選択肢②

 

以下のコード

$e_test = htmlspecialchars(filter_input(INPUT_GET, 'test'), ENT_QUOTES, 'UTF-8');
echo "{$e_test}";

によって、適切に入力がエスケープされるためXSSを防ぐ事ができる。
ただし、もしパラメーターtestに配列が入ってきている場合、またはパラメーターtestが存在しない場合は、filter_input()関数の戻り値の仕様が成功した場合は要求された変数の値、フィルタリングに失敗した場合にfalse、あるいは変数var_nameが設定されていない場合にnullを返します。
であるため、結果は次のとおりとなる。

Fatal error: Uncaught TypeError: htmlspecialchars(): Argument #1 ($string) must be of type string, null given in ...

 

 

まずはfilter_input関数について見ていきます。

// 指定した名前の変数を外部から受け取り、オプションでそれをフィルタリングする

filter_input(
    int $type,
    string $var_name,
    int $filter = FILTER_DEFAULT,
    array|int $options = 0
): mixed

/**
* $type:INPUT_GET, INPUT_POST, INPUT_SERVER, INPUT_COKKIE, INPUT_ENVのいずれか
* $var_name:取得する変数の名前
* $filter:
* 適用するフィルタのID。略した場合はFILTER_DEFAULTを使用。
* デフォルトでは何もフィルタリングをしない。
* $options:オプションあるいはフラグの論理和の連想配列
**/

 

選択肢のコードは、HTTP GETメソッドからtestパラメーターの値をフィルタリングし、HTMLエスケープしてから出力するものです。

しかしこのコードの問題点として、filter_input()関数が成功した場合は要求された変数の値を、フィルタリングに失敗した場合にfalseを、または変数が設定されていない場合にnullを返すことが挙げられます。

つまり、testパラメーターが配列の場合や、存在しない場合は、filter_input()関数がnullを返す可能性があります

そのため、nullが返された場合、htmlspecialchars()関数はエラーを引き起こしFatal errorが発生します。

これを避けるためには、filter_input()関数の後に、パラメータが存在するかどうかを確認し、nullをチェックする必要があります。

以上のことから、この選択肢は合っています。

 

選択肢③

 

以下のコード

$val = $_GET['test'] ?? '';

if ( is_string($val) ) {
    $ret = htmlentities($val, ENT_QUOTES, 'UTF-8');
    echo "{$ret}";
} else {
    foreach($val as $v) {
        echo "{$v}";
    }
}

をブラウザ経由で実行すると、test に文字列が入ってきても配列が入ってきても、適切に XSS 対策を行う事ができる。

 

 

まずはif文での判定に使用している「is_string()関数」について見ていきます。

$values = array('', ' ', '0', 0, false, true, null, array('false','true'));
foreach ($values as $value) {
    echo "is_string(";
    var_export($value);
    echo ") = ";
    echo var_dump(is_string($value));
}

/**出力結果
*is_string('') = bool(true)
*is_string(' ') = bool(true)
*is_string('0') = bool(true)
*is_string(0) = bool(false)
*is_string(false) = bool(false)
*is_string(true) = bool(false)
*is_string(NULL) = bool(false)
is_string(array (
  0 => 'false',
  1 => 'true',
)) = bool(false)
**/

以上より、変数$valにはnullが格納されることはなく、配列か配列でないかの判定となるため、配列の場合は分岐処理が行われ、パラメータ ‘test’ が存在しない場合でもエラーを回避することができます。

よって、この選択肢は合っています。

 

選択肢④

 

$_GET['test']に文字列で任意のユーザ入力が入っている (配列ではない) とした時に、以下のコード

$ret = htmlspecialchars($_GET['test'], ENT_COMPAT);
echo "{$ret}";

をブラウザ経由で実行すると、シングルクオートが変換されないため、XSS が発生する可能性がある。この実装は適切ではない。

 

このコードでは、htmlspecialchars()関数の第二引数にENT_COMPATを指定していますが、これはシングルクオートを変換しないようにするためのものであり、ダブルクオートを変換するためのものです。

このため、ユーザーが入力した文字列内にシングルクオートが含まれる場合、それらはエスケープされずに出力され、悪意のあるスクリプトが実行される可能性があります。

適切な実装では、htmlspecialchars()関数の第二引数に ENT_QUOTESを指定し、シングルクオートとダブルクオートの両方をエスケープする必要があります。

よって、この選択肢は合っています。

試験問題18

 

ファイルアップロード に関する説明の中で、誤っているものを1つ選びなさい。
なお「¥」はバックスラッシュに読み替えること。

 

選択肢①

 

PHPでファイルアップロードをする場合、HTMLのformには必ず「enctype=”multipart/form-data”」「method=”POST”」を指定する必要がある。
これらを忘れると、ファイルを取得する事ができない。
そのため、以下のHTML

をブラウザで閲覧して以下のコード

declare(strict_types=1);
error_reporting(-1);

var_dump($_FILES);

を実行すると

array(0) {
}

となる。

 

$_FILESは、アップロードしたファイルに関する情報を取得するためのスーパーグローバル変数で、この変数には以下のようなキーが含まれています。

name:アップロードされたファイルの元の名前
type:アップロードされたファイルのMIMEタイプ
tmp_name:アップロードされたファイルの一時ファイル名
error:アップロードされたファイルに関するエラー情報
size:アップロードされたファイルのサイズ

例えば、画像ファイルをアップロードするときのHTMLフォームは以下のように記述する必要があります。

<!-- file1.php -->
<!-- データのエンコード方式である enctype は、必ず以下のようにしなければなりません -->

<form enctype="multipart/form-data" action="file2.php" method="POST">
    <!-- MAX_FILE_SIZE は、必ず "file" input フィールドより前になければなりません -->
    <input type="hidden" name="MAX_FILE_SIZE" value="30000" />
    <!-- input 要素の name 属性の値が、$_FILES 配列のキーになります -->
    このファイルをアップロード: <input name="userfile" type="file" />
    <input type="submit" value="ファイルを送信" />
</form>
HTML入力フォームについて

enctype=”multipart/form-data”
ファイルをアップロードするためには、フォームのエンコード方式を “multipart/form-data” に設定する必要があります。これは、通常のフォームとは異なり、バイナリデータを送信するための方法です。

input type=”hidden” name=”MAX_FILE_SIZE” value=”30000″
フォームでアップロードできるファイルの最大サイズを制限する場合は、MAX_FILE_SIZE パラメータを指定します。この例では、30000 バイト (30KB) を指定しています。注意点として、このパラメータは “file” input フィールドよりも前に指定する必要があります。

input name=”userfile” type=”file”
ファイルをアップロードする input フィールドは、type=”file” を指定します。また、name 属性には、後でファイルを処理するために使用する、$_FILES 配列のキーを指定します。

method=”POST” action=”URL”
フォームが送信される HTTP メソッドと送信先の URL を指定します。この例では、POST メソッドで、送信先の URL は “URL” としています。

input type=”submit” value=”ファイルを送信”
フォームを送信するためのボタンです。この例では、”ファイルを送信” というラベルを指定しています。

よって、この選択肢は合っています。

 

選択肢②

 

PHPでファイルアップロードされた時に、アップロードされたファイルがサーバー上で保存されているテンポラリファイルの名前は
$_FILES[‘formのnameの値’][‘tmp_name’]
に入っている。
そのため、アップロードされたファイルを新しい位置に移動する move_uploaded_file() 関数の第一引数として適切に使う事ができる。
そのため、以下のHTML

をブラウザで閲覧して以下のコード

declare(strict_types=1);
error_reporting(-1);

var_dump($_FILES['f']['tmp_name']);
var_dump(is_readable($_FILES['f']['tmp_name']) );

を実行すると

string(14) "/tmp/php0stFZs"
bool(true)

となる(ファイル名は実行毎に変わる)。

 

$_FILESの連想配列の中身は以下のようになっています。

// 値は全て仮の値
Array(
    ['name'] => "test.csv"
    ['type'] => "text/plain"
    ['tmp_name'] => "/tmp/php5dkdaFd"
    ['error'] => "0"
    ['size'] => "123"
)

選択肢では、アップロードされたファイルが一時的に保存される場所「テンポラリファイル」と、そのファイルを新しい位置に移動するために使用する関数move_uploaded_file()について説明しています。

また、var_dump() 関数を使用して、$_FILES[‘f’][‘tmp_name’]の値を表示し、is_readable()関数を使用して、ファイルが読み取り可能であることを確認しています。

$_FILES[‘formのnameの値’][‘tmp_name’] は、アップロードされたファイルが保存されているテンポラリファイルのパスを表します。このパスを使用して、move_uploaded_file()関数の第一引数として適切に指定することで、アップロードされたファイルを新しい位置に移動することができます。

//fromで指定されたファイルが有効な場合、toで指定した移動先に移動される。
move_uploaded_file(string $from, string $to): bool

 

公式リファレンスでは複数のファイルのアップロードについて以下のように例が挙げられています。

$uploads_dir = '/uploads';
foreach ($_FILES["pictures"]["error"] as $key => $error) {
    if ($error == UPLOAD_ERR_OK) {
        $tmp_name = $_FILES["pictures"]["tmp_name"][$key];
        // basename() で、ひとまずファイルシステムトラバーサル攻撃は防げるでしょう。
        // ファイル名についてのその他のバリデーションも、適切に行いましょう。
        $name = basename($_FILES["pictures"]["name"][$key]);
        move_uploaded_file($tmp_name, "$uploads_dir/$name");
    }
}

このコードは、フォームから送信された複数のファイルを、一時ファイルからサーバー上の指定されたディレクトリにアップロードするための処理を行っています。

foreachループを使用して、送信されたファイルの数だけ処理を繰り返し、ループ内ではUPLOAD_ERR_OK(アップロード成功)である場合にのみファイルを処理するようにしています。

ループ内で、一時ファイルのパスを$tmp_name変数に、ファイル名を$name変数に取得しています。ここで、basename()関数を使用してファイル名から不正な文字を削除し、ファイルシステムトラバーサル攻撃を防止しています。

最後に、move_uploaded_file()関数を使用して、一時ファイルから指定されたディレクトリにファイルを移動しています。

このコードにはファイル名に対するその他のバリデーションが行われていないためセキュリティ上の懸念があります。例えば、アップロードされたファイルの拡張子を確認して許可されている拡張子であることを確認するなどの追加のバリデーションが必要です。

よって、この選択肢は合っています。

 

選択肢③

 

PHPでファイルアップロードされた時に、クライアントマシンの元のファイル名は
$_FILES[‘formのnameの値’][‘name’]
に入っている。
そのため、アップロードされたファイルを新しい位置に移動する move_uploaded_file() 関数の第二引数として適切に使う事ができる。
そのため、以下のHTML

をブラウザで閲覧して以下のコード

declare(strict_types=1);
error_reporting(-1);

var_dump($_FILES['f']['name']);
var_dump(is_readable($_FILES['f']['name']) );

を実行すると

string(8) "exam.txt"
bool(true)

となる。

 

この説明は、PHPでファイルアップロード時に、フォームから送信されたファイル名を取得する方法と、そのファイルを新しい場所に移動するために使用できるmove_uploaded_file()関数について説明しています。

フォームから送信されたファイル名は、$_FILES[‘フォームのname属性の値’][‘name’]という変数に格納されます。

よりイメージしやすくするためにコード例を見ていきます。(TechAcademyからの引用)

前提

アップロードファイル名:upload_csv
ファイルタイプ:csvファイル
格納先ディレクトリ:/var/www/files/
保存されたファイル名:upload_csv2023_04_08

// ファイルの保存先
$storeDir = '/var/www/files/';

// ファイルをアップロードする際は基本POSTメソッドを指定するので、それ以外の値が指定されていないかチェックします
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    exit('POSTメソッドを指定してください');
}

// アップロードが成功していれば[""error""]には0(=UPLOAD_ERR_OK)が格納されています
if ($_FILES['upload_csv']['error'] !== UPLOAD_ERR_OK) {
    exit('アップロードが失敗しました');
}

// ['type']にはアップロードされたファイルのコンテンツタイプが格納されています
// そのため、アップロードされたファイルがCSVファイルであるかチェックします
if ($_FILES['upload_csv']['type'] !== 'text/csv') {
    exit('CSVファイルをアップロードしてください');
}

// 現在時刻を元に唯一の値を生成してファイル名にします
$filename = date('Y_m_d');
// ファイルを一時フォルダから指定したディレクトリに移動します
move_uploaded_file($_FILES['upload_csv']['tmp_name'], $_FILES['upload_csv']['name'].$filename);

 

move_uploaded_file()関数の第二引数には、ディレクトリを含めたファイルパスを記述する必要があります。

ファイル名だけを指定した場合、ファイルはスクリプトが実行されたディレクトリに保存されます。

この時ファイル名は、アップロードされたファイルが特定のディレクトリに保存されることを確認するために、ディレクトリ名とファイル名を組み合わせたパスを指定することが一般的です。

選択肢では、アップロードされたファイルを新しい位置に移動させて適切にmove_uploaded_file()関数を使用しているわけではありません。

よって、この選択肢は誤りです。

 

選択肢④

 

PHPで複数のファイルをアップロードする時には、formに複数の「type=”file”」が、異なるnameアトリビュート値(または配列)であれば受け取る事ができる。
そのため、以下のHTML

をブラウザで閲覧して以下のコード

declare(strict_types=1);
error_reporting(-1);

var_dump($_FILES['userfile']['tmp_name']);
var_dump($_FILES['file_1']['tmp_name']);
var_dump($_FILES['file_2']['tmp_name']);

を実行すると

array(2) {
  [0]=>
  string(14) "/tmp/phpdmFh86"
  [1]=>
  string(14) "/tmp/phpBOFw0X"
}
string(14) "/tmp/phpZP2LSO"
string(14) "/tmp/php3GG1KF"

となる(ファイル名は実行毎に変わる)。

 

この説明は、PHPで複数のファイルをアップロードする方法について説明しています。

複数のファイルをアップロードする場合、HTMLのフォームに複数の「type=”file”」を指定することができます。

各ファイルのinputタグには異なるname属性の値を指定する必要があります。このようにすることで、PHPでは、$_FILESスーパーグローバル変数を使用して、各ファイルの情報を取得することができます。

<!-- 以下に設定する値は全て仮の設定値 -->
<form action="file-upload.php" method="post" enctype="multipart/form-data">
  Send these files:<br />
  <!-- inputタグにmultiple属性を設定することで、ブラウザのダイアログで複数のファイルが選択できるようになる。-->
  <input name="userfile[]" type="file" multiple="multiple" /><br />
  <input name="file_1" type="file" /><br />
  <input type="submit" value="Send files" />
</form>

<!-- スーパーグローバル変数$_FILESにセットされる値

①<userfile[]の場合>
Array (
  [upfile] => Array (
          [name] => Array (
                [0] => userfile_1.csv
                [1] => userfile_2.csv
          )
          [tmp_name] => Array (
                [0] => /tmp/phpdmFh86
                [1] => /tmp/phpBOFw0X
          )
  )
)

②<file1の場合>
Array (
  [upfile] => Array (
          [name] => file_1
          [tmp_name] => /tmp/phpZP2LSO
  )
)
-->

<userfile[]の場合>では、ファイル単位に格納されているわけではなく、$_FILESの取得可能な情報名ごとに格納されている点に注意!

よって、この選択肢は合っています。

ABOUT ME
ヒロ
社会人4年目/25歳/食品商社で2年間営業した後、IT業界にシステムエンジニアとして転職/Java,PHP言語を扱う開発エンジニア