web-dev-qa-db-ja.com

完全にセキュアな画像アップロードスクリプト

これが起こるかどうかはわかりませんが、試してみます。

過去1時間、画像アップロードの安全性について調査しました。アップロードをテストする機能がたくさんあることを学びました。

私のプロジェクトでは、画像をアップロードしても安全である必要があります。また、非常に多くの帯域幅が必要になる場合があり、多くの帯域幅を必要とする可能性があるため、APIを購入することはオプションではありません。

そこで、本当に安全な画像アップロード用の完全なPHPスクリプトを取得することにしました。本当に安全なものを見つけるのは不可能だからです。しかし、私はphpの専門家ではないため、いくつかの機能を追加するのは本当に頭痛の種です。このコミュニティの助けを借りて、本当に安全な画像アップロードの完全なスクリプトを作成してください。

それについての本当に素晴らしいトピックはここにあります(ただし、彼らはトリックを行うために必要なことだけを伝えていますが、これを行う方法は教えていません。自分で): PHPイメージアップロードセキュリティチェックリストhttps://security.stackexchange.com/questions/32852/risks-of-a-php-image-upload-form =

要約すると、彼らはこれがセキュリティイメージのアップロードに必要なものだと言っています(上記のページから引用します)。

  • .httaccessを使用して、PHPがアップロードフォルダー内で実行されないようにします。
  • ファイル名に文字列「php」が含まれている場合、アップロードを許可しないでください。
  • 拡張子のみを許可:jpg、jpeg、gifおよびpng。
  • 画像ファイルタイプのみを許可します。
  • 2つのファイルタイプの画像を許可しません。
  • イメージ名を変更します。ルートディレクトリではなくサブディレクトリにアップロードします。

また:

  • Gd(またはImagick)を使用して画像を再処理し、処理された画像を保存します。他のすべては、ハッカーにとって退屈なだけです」
  • Rrが指摘したように、アップロードにはmove_uploaded_file()を使用してください」
  • ところで、アップロードフォルダについては非常に制限したいでしょう。これらの場所は、多くの悪用が行われる暗いコーナーの1つです
    happen。これは、あらゆるタイプのアップロードとプログラミングに有効です
    言語/サーバー。小切手
    https://www.owasp.org/index.php/Unrestricted_File_Upload
  • レベル1:拡張子を確認します(拡張子ファイルの末尾は)
  • レベル2:MIMEタイプの確認($ file_info = getimagesize($ _ FILES ['image_file']; $ file_mime = $ file_info ['mime'];)
  • レベル3:最初の100バイトを読み取り、次の範囲のバイトがあるかどうかを確認します:ASCII 0-8、12-31(10進数)。
  • レベル4:ヘッダー内のマジックナンバー(ファイルの最初の10〜20バイト)を確認します。ここからいくつかのファイルヘッダーバイトを見つけることができます。
    http://en.wikipedia.org/wiki/Magic_number_%28programming%29#Examples
  • $ _FILES ['my_files'] ['tmp_name']でも「is_uploaded_file」を実行することをお勧めします。見る
    http://php.net/manual/en/function.is-uploaded-file.php

ここにその大部分がありますが、それだけではありません。 (アップロードをさらに安全にするのに役立つ何かを知っている場合は、共有してください。)

THIS IS WHAT WE GOT NOW

  • メインPHP:

    function uploadFile ($file_field = null, $check_image = false, $random_name = false) {
    
    //Config Section    
    //Set file upload path
    $path = 'uploads/'; //with trailing slash
    //Set max file size in bytes
    $max_size = 1000000;
    //Set default file extension whitelist
    $whitelist_ext = array('jpeg','jpg','png','gif');
    //Set default file type whitelist
    $whitelist_type = array('image/jpeg', 'image/jpg', 'image/png','image/gif');
    
    //The Validation
    // Create an array to hold any output
    $out = array('error'=>null);
    
    if (!$file_field) {
      $out['error'][] = "Please specify a valid form field name";           
    }
    
    if (!$path) {
      $out['error'][] = "Please specify a valid upload path";               
    }
    
    if (count($out['error'])>0) {
      return $out;
    }
    
    //Make sure that there is a file
    if((!empty($_FILES[$file_field])) && ($_FILES[$file_field]['error'] == 0)) {
    
    // Get filename
    $file_info = pathinfo($_FILES[$file_field]['name']);
    $name = $file_info['filename'];
    $ext = $file_info['extension'];
    
    //Check file has the right extension           
    if (!in_array($ext, $whitelist_ext)) {
      $out['error'][] = "Invalid file Extension";
    }
    
    //Check that the file is of the right type
    if (!in_array($_FILES[$file_field]["type"], $whitelist_type)) {
      $out['error'][] = "Invalid file Type";
    }
    
    //Check that the file is not too big
    if ($_FILES[$file_field]["size"] > $max_size) {
      $out['error'][] = "File is too big";
    }
    
    //If $check image is set as true
    if ($check_image) {
      if (!getimagesize($_FILES[$file_field]['tmp_name'])) {
        $out['error'][] = "Uploaded file is not a valid image";
      }
    }
    
    //Create full filename including path
    if ($random_name) {
      // Generate random filename
      $tmp = str_replace(array('.',' '), array('',''), microtime());
    
      if (!$tmp || $tmp == '') {
        $out['error'][] = "File must have a name";
      }     
      $newname = $tmp.'.'.$ext;                                
    } else {
        $newname = $name.'.'.$ext;
    }
    
    //Check if file already exists on server
    if (file_exists($path.$newname)) {
      $out['error'][] = "A file with this name already exists";
    }
    
    if (count($out['error'])>0) {
      //The file has not correctly validated
      return $out;
    } 
    
    if (move_uploaded_file($_FILES[$file_field]['tmp_name'], $path.$newname)) {
      //Success
      $out['filepath'] = $path;
      $out['filename'] = $newname;
      return $out;
    } else {
      $out['error'][] = "Server Error!";
    }
    
     } else {
      $out['error'][] = "No file uploaded";
      return $out;
     }      
    }
    
    
    if (isset($_POST['submit'])) {
     $file = uploadFile('file', true, true);
     if (is_array($file['error'])) {
      $message = '';
      foreach ($file['error'] as $msg) {
      $message .= '<p>'.$msg.'</p>';    
     }
    } else {
     $message = "File uploaded successfully".$newname;
    }
     echo $message;
    }
    
  • そして、フォーム:

    <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" enctype="multipart/form-data" name="form1" id="form1">
    <input name="file" type="file" id="imagee" />
    <input name="submit" type="submit" value="Upload" />
    </form>
    

したがって、私が求めているのは、コードのスニペットを投稿して、私(および他のすべての人)がこの画像アップロードスクリプトを作成して非常に安全にすることです。または、すべてのスニペットを追加した完全なスクリプトを共有/作成します。

42
Simon

セキュリティで保護された画像アップロードスクリプトの作成を開始する際には、考慮すべきことがたくさんあります。今、私はこれに関する専門家に近いところはありませんが、過去に一度これを開発するように頼まれました。あなたが従うことができるように、私はここで行ってきたプロセス全体を見ていきます。このために、ファイルを処理する非常に基本的なhtmlフォームとphpスクリプトから始めます。

HTMLフォーム:

<form name="upload" action="upload.php" method="POST" enctype="multipart/form-data">
    Select image to upload: <input type="file" name="image">
    <input type="submit" name="upload" value="upload">
</form>

PHPファイル:

<?php
$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
    echo "Image succesfully uploaded.";
} else {
    echo "Image uploading failed.";
}
?> 

最初の問題:ファイルタイプ
攻撃者は、Webサイトのフォームを使用してサーバーにファイルをアップロードする必要はありません。 POSTリクエストは、さまざまな方法で傍受できます。ブラウザのアドオン、プロキシ、Perlスクリプトについて考えてください。どんなに一生懸命努力しても、攻撃者が想定していないものをアップロードしようとするのを防ぐことはできません。そのため、セキュリティはすべてサーバー側で実行する必要があります。

最初の問題はファイルの種類です。上記のスクリプトでは、攻撃者は、たとえばphpスクリプトなど、必要なものをアップロードし、直接リンクに従って実行することができます。これを防ぐために、コンテンツタイプの検証を実装します

<?php
if($_FILES['image']['type'] != "image/png") {
    echo "Only PNG images are allowed!";
    exit;
}

$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
    echo "Image succesfully uploaded.";
} else {
    echo "Image uploading failed.";
}
?>

残念ながら、これでは十分ではありません。前述したように、攻撃者はリクエストを完全に制御できます。リクエストヘッダーの変更を妨げるものはなく、コンテンツタイプを「image/png」に変更するだけです。そのため、Content-typeヘッダーだけに頼るのではなく、アップロードされたファイルのコンテンツも検証することをお勧めします。ここで、php Gdライブラリが役立ちます。 getimagesize()を使用して、Gdライブラリで画像を処理します。画像でない場合、これは失敗し、そのためアップロード全体が失敗します。

<?php
$verifyimg = getimagesize($_FILES['image']['tmp_name']);

if($verifyimg['mime'] != 'image/png') {
    echo "Only PNG images are allowed!";
    exit;
}

$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
    echo "Image succesfully uploaded.";
} else {
    echo "Image uploading failed.";
}
?>

私たちはまだそこにいません。ほとんどの画像ファイルタイプでは、テキストコメントを追加できます。繰り返しますが、攻撃者がコメントとしてphpコードを追加することを妨げるものはありません。 Gdライブラリは、これを完全に有効な画像として評価します。 PHPインタープリターは、イメージを完全に無視し、コメント内のphpコードを実行します。どのファイル拡張子がphpインタープリターによって処理され、どのファイル拡張子が処理されないかはphpの構成に依存しますが、VPSの使用によりこの構成を制御できない多くの開発者がいるため、想定できませんPHPインタープリターは画像を処理しません。これが、ファイル拡張子のホワイトリストを追加するだけでも十分に安全でない理由です。

これに対する解決策は、攻撃者がファイルに直接アクセスできない場所に画像を保存することです。これは、ドキュメントルートの外部、または.htaccessファイルで保護されたディレクトリにあります。

order deny,allow
deny from all
allow from 127.0.0.1

編集:他のPHPプログラマーと話し合った後、htaccessは常に信頼できるとは限らないため、ドキュメントルート以外のフォルダーを使用することを強くお勧めします。

それでも、ユーザーまたは他の訪問者が画像を表示できるようにする必要があります。そのため、phpを使用してそれらの画像を取得します。

<?php
$uploaddir = 'uploads/';
$name = $_GET['name']; // Assuming the file name is in the URL for this example
readfile($uploaddir.$name);
?>

2番目の問題:ローカルファイルインクルージョン攻撃
スクリプトは今のところかなり安全ですが、サーバーが他の脆弱性に悩まされているとは想定できません。一般的なセキュリティの脆弱性は、ローカルファイルインクルージョンとして知られています。これを説明するには、サンプルコードを追加する必要があります。

<?php
if(isset($_COOKIE['lang'])) {
   $lang = $_COOKIE['lang'];
} elseif (isset($_GET['lang'])) {
   $lang = $_GET['lang'];
} else {
   $lang = 'english';
}

include("language/$lang.php");
?>

この例では、多言語Webサイトについて説明しています。サイトの言語は、「高リスク」情報と見なされるものではありません。訪問者にCookieまたはGETリクエストを介して優先言語を取得し、それに基づいて必要なファイルを含めるようにします。次に、攻撃者が次のURLを入力するとどうなるかを考えます。

www.example.com/index.php?lang=../uploads/my_evil_image.jpg

PHPには、攻撃者によってアップロードされたファイルが含まれます。これにより、攻撃者はファイルに直接アクセスできず、元の状態に戻ります。

この問題の解決策は、ユーザーがサーバー上のファイル名を知らないようにすることです。代わりに、データベースを使用してファイル名と拡張子を変更し、それを追跡します。

CREATE TABLE `uploads` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(64) NOT NULL,
    `original_name` VARCHAR(64) NOT NULL,
    `mime_type` VARCHAR(20) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;


<?php

if(!empty($_POST['upload']) && !empty($_FILES['image']) && $_FILES['image']['error'] == 0)) {

    $uploaddir = 'uploads/';

    /* Generates random filename and extension */
    function tempnam_sfx($path, $suffix){
        do {
            $file = $path."/".mt_Rand().$suffix;
            $fp = @fopen($file, 'x');
        }
        while(!$fp);

        fclose($fp);
        return $file;
    }

    /* Process image with Gd library */
    $verifyimg = getimagesize($_FILES['image']['tmp_name']);

    /* Make sure the MIME type is an image */
    $pattern = "#^(image/)[^\s\n<]+$#i";

    if(!preg_match($pattern, $verifyimg['mime']){
        die("Only image files are allowed!");
    }

    /* Rename both the image and the extension */
    $uploadfile = tempnam_sfx($uploaddir, ".tmp");

    /* Upload the file to a secure directory with the new name and extension */
    if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {

        /* Setup a database connection with PDO */
        $dbhost = "localhost";
        $dbuser = "";
        $dbpass = "";
        $dbname = "";

        // Set DSN
        $dsn = 'mysql:Host='.$dbhost.';dbname='.$dbname;

        // Set options
        $options = array(
            PDO::ATTR_PERSISTENT    => true,
            PDO::ATTR_ERRMODE       => PDO::ERRMODE_EXCEPTION
        );

        try {
            $db = new PDO($dsn, $dbuser, $dbpass, $options);
        }
        catch(PDOException $e){
            die("Error!: " . $e->getMessage());
        }

        /* Setup query */
        $query = 'INSERT INTO uploads (name, original_name, mime_type) VALUES (:name, :oriname, :mime)';

        /* Prepare query */
        $db->prepare($query);

        /* Bind parameters */
        $db->bindParam(':name', basename($uploadfile));
        $db->bindParam(':oriname', basename($_FILES['image']['name']));
        $db->bindParam(':mime', $_FILES['image']['type']);

        /* Execute query */
        try {
            $db->execute();
        }
        catch(PDOException $e){
            // Remove the uploaded file
            unlink($uploadfile);

            die("Error!: " . $e->getMessage());
        }
    } else {
        die("Image upload failed!");
    }
}
?>

そこで、次のことを行いました。

  • 画像を保存する安全な場所を作成しました
  • Gdライブラリで画像を処理しました
  • 画像のMIMEタイプを確認しました
  • ファイル名を変更し、拡張子を変更しました
  • データベースに新しいファイル名と元のファイル名の両方を保存しました
  • また、データベースにMIMEタイプを保存しました

訪問者に画像を表示できるようにする必要があります。これを行うには、単にデータベースのid列を使用します。

<?php

$uploaddir = 'uploads/';
$id = 1;

/* Setup a database connection with PDO */
$dbhost = "localhost";
$dbuser = "";
$dbpass = "";
$dbname = "";

// Set DSN
$dsn = 'mysql:Host='.$dbhost.';dbname='.$dbname;

// Set options
$options = array(
    PDO::ATTR_PERSISTENT    => true,
    PDO::ATTR_ERRMODE       => PDO::ERRMODE_EXCEPTION
);

try {
    $db = new PDO($dsn, $dbuser, $dbpass, $options);
}
catch(PDOException $e){
    die("Error!: " . $e->getMessage());
}

/* Setup query */
$query = 'SELECT name, original_name, mime_type FROM uploads WHERE id=:id';

/* Prepare query */
$db->prepare($query);

/* Bind parameters */
$db->bindParam(':id', $id);

/* Execute query */
try {
    $db->execute();
    $result = $db->fetch(PDO::FETCH_ASSOC);
}
catch(PDOException $e){
    die("Error!: " . $e->getMessage());
}

/* Get the original filename */
$newfile = $result['original_name'];

/* Send headers and file to visitor */
header('Content-Description: File Transfer');
header('Content-Disposition: attachment; filename='.basename($newfile));
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($uploaddir.$result['name']));
header("Content-Type: " . $result['mime_type']);
readfile($uploaddir.$result['name']);
?>

このスクリプトのおかげで、訪問者は画像を表示したり、元のファイル名でダウンロードしたりできます。ただし、(s)サーバー上のファイルに直接アクセスすることはできず、(s)サーバーをだまして自分のファイルにアクセスすることもできません。 。 (S)アップロードディレクトリをブルートフォースすることはできません。サーバー自体以外は誰もディレクトリにアクセスできないからです。

これで、安全な画像アップロードスクリプトが終了しました。

このスクリプトに最大ファイルサイズを含めなかったことを付け加えますが、それは自分で簡単に行えるはずです。

ImageUploadクラス
このスクリプトの需要が高いため、Webサイトの訪問者がアップロードした画像をすべてのユーザーが安全に処理できるようにするImageUploadクラスを作成しました。このクラスは、単一ファイルと複数ファイルの両方を一度に処理でき、画像の表示、ダウンロード、削除などの追加機能を提供します。

ここに投稿するコードは単純に大きいため、MEGAからクラスをダウンロードできます。

ImageUploadクラスのダウンロード

README.txtを読んで、指示に従ってください。

オープンソース化
Image Secureクラスプロジェクトは、私の Github プロファイルでも利用できるようになりました。これにより、他の人(あなた?)がプロジェクトに貢献し、これをすべての人にとって素晴らしいライブラリにすることができます。 (現在バグがあります。修正されるまで上記のダウンロードを使用してください)。

78
icecub

PHPにファイルをアップロードするのは簡単で安全です。以下について学ぶことをお勧めします。

  • pathinfo -ファイルパスに関する情報を返します
  • move_uploaded_file -アップロードされたファイルを新しい場所に移動します
  • copy -ファイルをコピーします
  • finfo_open -新しいfileinfoリソースを作成します

PHPにファイルをアップロードするには、PUTPOSTの2つの方法があります。 HTMLでPOSTメソッドを使用するには、次のようにフォームで enctype を有効にする必要があります。

<form action="" method="post" enctype="multipart/form-data">
  <input type="file" name="file">
  <input type="submit" value="Upload">
</form>

次に、PHPで、アップロードされたファイルを $_FILES で取得する必要があります。

$_FILES['file']

次に、move_uploaded_fileを使用してtemp( "upload")からファイルを移動する必要があります。

if (move_uploaded_file($_FILES['file']['tmp_name'], YOUR_PATH)) {
   // ...
}

ファイルをアップロードした後、ファイルの拡張子を確認する必要があります。これを行う最良の方法は、次のようにpathinfoを使用することです。

$extension = pathinfo($_FILES['file']['tmp_name'], PATHINFO_EXTENSION);

ただし、拡張子が.jpgのファイルをアップロードできますが、MIMEタイプはtext/phpであり、これはバックドアであるため、拡張子は安全ではありません。したがって、次のようにfinfo_openで実際のmimetypeを確認することをお勧めします。

$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['file']['tmp_name']);

また、$_FILES['file']['type']を使用しないでください。ブラウザとクライアントOSによっては、application/octet-streamを受け取る場合があり、このmimetypeはアップロードされたファイルの実際のmimetypeではありません。

このシナリオでファイルを安全にアップロードできると思います。

さようなら、私の英語でごめんなさい!