54
65

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ディレクトリ作成を許可してファイルをアップロード

Last updated at Posted at 2014-02-23

前書き

まだ 「ファイルアップロードの例外処理はこれぐらいしないと気が済まない」 をご覧になっていない方は先にそちらからどうぞ。

タイトルの通り、ディレクトリ作成を許可してユーザーにファイルをアップロードさせたいときのためのスクリプトです。今回は以下の条件のもと行ってみます。

  • ファイル名はそのまま使用する
  • 許可するファイル形式を JPEG PNG GIF のみに制限する
  • ファイル名またはディレクトリ名に使われる文字を正規表現 [\w.-] にマッチするもののみに制限する
  • スクリプトとして実行可能な *.php *.cgi *.rb *.py にマッチするファイル名のものは禁止する (トロイの木馬対策)
  • ディレクトリを10階層までに制限する
  • . で始まるまたは終わる「ファイル」「2文字以上のディレクトリ」は許可しない (ディレクトリトラバーサル対策)
  • 絶対パスによるディレクトリ指定は許可しない

ソースコード

コピペで動くと思います。サーバーに他の実行可能な拡張子が存在する場合はそれも禁止対象に追加してください。

<?php

/* HTML特殊文字をエスケープする関数 */
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

// 全てのパラメータを正しい構造で受け取った時のみ実行
if (
    isset($_POST['path'], $_FILES['upfile']['error']) &&
    is_int($_FILES['upfile']['error']) &&
    is_string($_POST['path'])
) {
    
    try {
        
        /* ファイルのチェック */
        switch ($_FILES['upfile']['error']) {
            case UPLOAD_ERR_OK:
                // エラー無し
                break;
            case UPLOAD_ERR_NO_FILE:
                // ファイル未選択
                throw new RuntimeException('File is not selected');
            case UPLOAD_ERR_INI_SIZE:
            case UPLOAD_ERR_FORM_SIZE:
                // 許可サイズを超過
                throw new RuntimeException('File is too large');
            default:
                throw new RuntimeException('Unknown error');
        }
        if (!in_array(
            @exif_imagetype($_FILES['upfile']['tmp_name']),
            array(
                IMAGETYPE_GIF,
                IMAGETYPE_JPEG,
                IMAGETYPE_PNG,
            ),
            true
        )) {
            // JPEG, PNG, GIF ではない
            throw new RuntimeException('Unsupported image format');
        }
        if (!preg_match('/\A(?!\.)[\w.-]++(?<!\.)\z/', $_FILES['upfile']['name'])) {
            // 無効なファイル名
            throw new RuntimeException('Invalid filename: ' . $_FILES['upfile']['name']);
        }
        if (!preg_match('/(?<!\.php)(?<!\.cgi)(?<!\.py)(?<!\.rb)\z/i', $_FILES['upfile']['name'])) {
            // トロイの木馬を弾くため、実行可能な拡張子は禁止する
            throw new RuntimeException(
                'This extension is forbidden for security reason: ' .
                $_FILES['upfile']['name']
            );
        }
        
        /* ディレクトリの生成 */
        $deep = 0;
        foreach (explode('/', $_POST['path']) as $i => $dir) {
            if ($deep > 10) {
                // 10階層を超過
                throw new RuntimeException('Hierarchy is too deep');
            }
            if ($dir === '') {
                if ($_POST['path'] !== '' && !$i) {
                    // 絶対パスを検知
                    throw new RuntimeException('Absolute path is not allowed');
                }
                // 空文字列はスキップ
                continue;
            }
            if ($dir === '.') {
                // 「.」はスキップする
                continue;
            }
            if (!preg_match('/\A(?!\.)[\w.-]++(?<!\.)\z/', $dir)) {
                // 無効なディレクトリ名
                throw new RuntimeException('Invalid directory name: ' . $dir);
            }
            if (!is_dir($dir)) {
                // ディレクトリが存在していなければ生成を試みる
                if (!mkdir($dir)) {
                    // ディレクトリ生成に失敗
                    throw new RuntimeException('Failed to create directory: ' . $dir);
                }
                // パーミッションを0777に設定
                chmod($dir, 0777);
                $msgs[] = array('blue', 'Created directory "' . $dir . '"');
            }
            // カレントディレクトリを移動
            chdir($dir);
            ++$deep;
        }
        
        /* ファイルの移動 */
        if (!move_uploaded_file($_FILES['upfile']['tmp_name'], $_FILES['upfile']['name'])) {
            // ファイル移動に失敗
            throw new RuntimeException('Failed to save uploaded file');
        }
        // ファイルのパーミッションを確実に0644に設定する
        chmod($_FILES['upfile']['name'], 0644);
        
        /* メッセージをセット */
        $msgs[] = array(
            'green',
            'Uploaded successfully: ' .
                ($_POST['path'] === '' ? '.' : $_POST['path']) .
                '/' .
                $_FILES['upfile']['name']
        );

    } catch (RuntimeException $e) {

        $msgs[] = array('red', $e->getMessage());

    }

}

// XHTMLとしてブラウザに認識させる
// (IE8以下はサポート対象外w)
header('Content-Type: application/xhtml+xml; charset=utf-8');

?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Hierarchical Image Uploading</title>
</head>
<body>
<?php if (isset($msgs)): ?>
  <fieldset>
    <legend>Result</legend>
    <ul>
<?php foreach ($msgs as $msg): ?> 
      <li style="color:<?=h($msg[0])?>;"><?=h($msg[1])?></li>
<?php endforeach; ?>
    </ul>
  </fieldset>
<?php endif; ?>
  <form enctype="multipart/form-data" method="post" action="">
    <fieldset>
      <legend>Select file (Directory name and filename must match <strong>'/\A(?!\.)[\w.-]++(?&lt;!\.)\z/'</strong>)</legend>
      Directory path: <input type="text" name="path" value="" /><br />
      Filename(GIF, JPEG, PNG): <input type="file" name="upfile" /><br />
      <input type="submit" value="Upload" />
    </fieldset>
  </form>
</body>
</html>
54
65
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
65

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?