is_uploaded_file() / move_uploaded_file() の必要性？


まだ **[「ファイルアップロードの例外処理はこれぐらいしないと気が済まない」](http://qiita.com/mpyw/items/939964377766a54d4682)** をご覧になっていない方は先にそちらからどうぞ。

# **問題提起**

ファイルアップロード関連の記事を書いているとき、いつも疑問に思っていることがあった。

![ss (2014-03-21 at 11.12.04).png](https://qiita-image-store.s3.amazonaws.com/0/25060/c7e60768-10b8-3e36-75c7-3034cd1dc5c3.png)

![ss (2014-03-21 at 11.12.45).png](https://qiita-image-store.s3.amazonaws.com/0/25060/d44188ba-d82b-c784-eec1-379076d57cfd.png)

だからさぁ、 **アップロードされなかったファイル** の名前がどうやったら **`$_FILES['upfile']['tmp_name']`** に混入できるんだよ！？このチェックいるのかホントに！？

なんて思いながら「PHPマニュアルが勧めているから」という理由で訳も分からず記事を書いていた。

# **歴史的な理由**

そう、実はこの関数の背景には **歴史的な理由** があったのだ… **[register_globals](http://www.php.net/manual/ja/security.globals.php)** という害悪機能の存在だ。もしこの機能が有効な場合、以下のようなURLでリクエストを受けたとき、不正に `/etc/passwd` を閲覧される可能性がある。

```php
<?php
printf(
    '<a href="http://example.com/upload.php?%s">不正ファイル作成</a>',
    http_build_query(array(
        '_FILES' => array(
            'upfile' => array(
                'name' => 'passwd.txt',
                'tmp_name' => '/etc/passwd',
                'error' => UPLOAD_ERR_OK,
            )
        )
    ))
);
```

但し、最早この機能が有効になっている環境に遭遇すること自体がレアであり、よく分からないようなレンタルサーバーであっても **PHP 5** であるとさえ書いてくれてあればおそらくほぼ全てのものが無効とされているだろう。というか今更、サポート切れてて、なんちゃってオブジェクト指向しか扱えないような **PHP 4** みたいな欠陥クソ言語使うな、うん。

# **PHP言語レベルでの考察**

## **[register_globals](http://www.php.net/manual/ja/security.globals.php) との向き合い方**

**[register_globals](http://www.php.net/manual/ja/security.globals.php)** がphp.iniのディレクティブから削除されたのは **PHP 5.4.0** のときである。

![ss (2014-03-21 at 11.38.43).png](https://qiita-image-store.s3.amazonaws.com/0/25060/395b540a-9235-d7dd-2f86-235743c28dd7.png)

php.iniの設定次第で致命的な脆弱性が生まれてしまうのは気持ち悪く感じるので、これより古いバージョンを使用している場合はやはりこういったチェックを行ったほうがいいように思える…が、<ins>ファイルアップロード以外にも **[register_globals](http://www.php.net/manual/ja/security.globals.php)** の存在を想定していないコードは山ほどある</ins>よな…別にここだけやったところで特に意味ないよな…

## **上書きに関する挙動の違い**

但し、以下のような上書きに関する挙動の違いがあるので、 **[move_uploaded_file](http://www.php.net/manual/ja/function.move-uploaded-file.php)** 関数に限っては必要と言えるかもしれない。上書きが行えるアップロードファイルの移動が可能な関数はこれしか存在しない。もちろん

- テンポラリファイルはスクリプト実行後すぐ削除されてしまうからコピーでも十分
- そもそもWindowsでPHP使ってないから大丈夫

と言われればそれまでではあるが…

#### OSを問わず上書きする

- **[move_uploaded_file](http://www.php.net/manual/ja/function.move-uploaded-file.php)** 関数
- **[copy](http://www.php.net/manual/ja/function.copy.php)** 関数
- **[imagepng](http://www.php.net/manual/ja/function.imagepng.php)** 関数
- **[imagejpeg](http://www.php.net/manual/ja/function.imagejpeg.php)** 関数
- **[imagegif](http://www.php.net/manual/ja/function.imagegif.php)** 関数

#### Windows上でのみ **Warning** を発生して処理を中止する

- **[rename](http://www.php.net/manual/ja/function.rename.php)** 関数

# **C言語レベルでの考察**

```c:php_do_open_temporary_file関数(テンポラリファイル生成に関与)
static int php_do_open_temporary_file(/* 省略 */) {
    
    /* 省略 */
    
#ifndef HAVE_MKSTEMP
    int open_flags = O_CREAT | O_TRUNC | O_RDWR
#ifdef PHP_WIN32
        | _O_BINARY
#endif
        ;
#endif
    
    /* 省略 */

#ifdef PHP_WIN32
    if (GetTempFileName(new_state.cwd, pfx, 0, opened_path)) {
        /* Some versions of windows set the temp file to be read-only,
        * which means that opening it will fail... */
        if (VCWD_CHMOD(opened_path, 0600)) {
            efree(opened_path);
            efree(new_state.cwd);
            return -1;
        }
        fd = VCWD_OPEN_MODE(opened_path, open_flags, 0600);
    }
#elif defined(HAVE_MKSTEMP)
    fd = mkstemp(opened_path);
#else
    if (mktemp(opened_path)) {
        fd = VCWD_OPEN(opened_path, open_flags);
    }
#endif
    
    /* 省略 */
    
}
```

```c:VCWD_*のマクロ定義
#define VCWD_OPEN(path, flags) virtual_open(path TSRMLS_CC, flags)
#define VCWD_OPEN_MODE(path, flags, mode) virtual_open(path TSRMLS_CC, flags, mode)
#define VCWD_CREAT(path, mode) virtual_creat(path, mode TSRMLS_CC)
```

```c:virtual_open関数(open関数のラッパー)
CWD_API int virtual_open(const char *path TSRMLS_DC, int flags, ...) {
    
    /* 省略 */
    
    if (flags & O_CREAT) {
        mode_t mode;
        va_list arg;
        va_start(arg, flags);
        mode = (mode_t) va_arg(arg, int);
        va_end(arg);
        f = open(new_state.cwd, flags, mode);
    } else {
        f = open(new_state.cwd, flags);
    }
    
    /* 省略 */
    
}
```

```c:move_uploaded_file関数のC言語レベルでの実装
PHP_FUNCTION(move_uploaded_file) {
    
    /* 省略 */

    if (!SG(rfc1867_uploaded_files)) {
        RETURN_FALSE;
    }
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &path, &path_len, &new_path, &new_path_len) == FAILURE) {
        return;
    }
    if (!zend_hash_exists(SG(rfc1867_uploaded_files), path, path_len + 1)) {
        RETURN_FALSE;
    }

    /* 省略 */

    if (VCWD_RENAME(path, new_path) == 0) {
        successful = 1;
#ifndef PHP_WIN32
        oldmask = umask(077);
        umask(oldmask);
        ret = VCWD_CHMOD(new_path, 0666 & ~oldmask);
        if (ret == -1) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "%s", strerror(errno));
        }
#endif
    } else if (php_copy_file_ex(path, new_path, STREAM_DISABLE_OPEN_BASEDIR TSRMLS_CC) == SUCCESS) {
        VCWD_UNLINK(path);
        successful = 1;
    }

    /* 省略 */        

}
```

## **テンポラリファイルのパーミッションは必ず `0600` になる（？）**

### **Windows環境の場合**

WindowsAPIの `GetTempFileName` 関数はテンポラリファイルを作成するが、作成直後にパーミッションを `0600` に手動変更しているのが見て取れる。

### **`mkstemp` 関数が利用可能なLinux環境の場合**

現在のほとんどの環境で使用されているglibcはバージョン2.0.7以降という条件を満たしており、その場合はパーミッション `0600` として作成される。

### **`mkstemp` 関数が利用できないLinux環境の場合**

「そもそもこんな環境が存在するのか…？」と疑問に感じたが、一応そのケースでコールされている `VCWD_OPEN` マクロを辿ってみると、何やら存在しない引数を得ようとしてしまうようなコードが目に入る。 `O_CREAT` をフラグに指定している場合はそもそも使うべきマクロは `VCWD_OPEN_MODE` であり、これでパーミッションを第3引数に指定する必要がある。もしくは `VCWD_CREAT` を使用する必要がある。もし強引に `VCWD_OPEN` を利用してファイルを作成しようとした場合、 **パーミッションの値は未定義** となる。おそらくバグだと思われる。

## **[move_uploaded_file](http://www.php.net/manual/ja/function.move-uploaded-file.php) 関数・ [is_uploaded_file](http://www.php.net/manual/ja/function.is-uploaded-file.php) 関数はどちらか1つで十分**

コメント欄で指摘されている通り、この関数の最初のブロックでは **[is_uploaded_file](http://www.php.net/manual/ja/function.is-uploaded-file.php)** 関数と同等のチェックが行われているため、 **[is_uploaded_file](http://www.php.net/manual/ja/function.is-uploaded-file.php)** とこの関数を併用する必要性は例え **[register_globals](http://www.php.net/manual/ja/security.globals.php)** が有効であったとしても皆無である。

## **移動後のファイルのパーミッションに一貫性が無い**

**重要な前提条件**
ファイル内容が上書きされるとき、<ins>パーミッションまでは上書きされない</ins>。

`VCWD_RENAME` マクロをコールしているところに着目してほしい。リネームが成功したとき、Windows環境でなければその下のパーミッション変更処理が適用される。この変更は **PHP5.2.5** のときに行われたようだ。

失敗したときには手段が変わるが、本質的にはリネームと同等の処理が行われるようになっている。 **[open_basedir](http://www.php.net/manual/ja/ini.core.php#ini.open-basedir)** ディレクティブが関係しているようだが、ここで重要なのは<ins>後者のフローではパーミッション変更処理が行われない</ins>ことにある。これによって以下のような差異が生まれてしまう。

### **PHP5.2.5以降**

| | **[copy](http://www.php.net/manual/ja/function.copy.php)** | **[move_uploaded_file](http://www.php.net/manual/ja/function.move-uploaded-file.php)** |
|:--:|:--:|:--:|
| `php_copy_file_*` 関数による<br />新規コピー | **[umask](http://www.php.net/manual/ja/function.umask.php)** 設定値が<br />適用される | **[umask](http://www.php.net/manual/ja/function.umask.php)** 設定値が<br />適用される |
| `VCWD_RENAME` マクロによる<br />新規リネーム | - | **[umask](http://www.php.net/manual/ja/function.umask.php)** 設定値が<br />適用される |
| `php_copy_file_*` 関数による<br />上書きコピー | もとのパーミッションが<br />維持される | もとのパーミッションが<br />維持される |
| `VCWD_RENAME` マクロによる<br />上書きリネーム | - | **[umask](http://www.php.net/manual/ja/function.umask.php)** 設定値が<br />適用される |

### **PHP5.2.4以前**

| | **[copy](http://www.php.net/manual/ja/function.copy.php)** | **[move_uploaded_file](http://www.php.net/manual/ja/function.move-uploaded-file.php)** |
|:--:|:--:|:--:|
| `php_copy_file_*` 関数による<br />新規コピー | **[umask](http://www.php.net/manual/ja/function.umask.php)** 設定値が<br />適用される | **[umask](http://www.php.net/manual/ja/function.umask.php)** 設定値が<br />適用される |
| `VCWD_RENAME` マクロによる<br />新規リネーム | - | もとのパーミッションが<br />維持される |
| `php_copy_file_*` 関数による<br />上書きコピー | もとのパーミッションが<br />維持される | もとのパーミッションが<br />維持される |
| `VCWD_RENAME` マクロによる<br />上書きリネーム | - | もとのパーミッションが<br />維持される |

検証してみたところ、手元の **PHP5.5.10** の環境では `php_copy_file_*` 関数による上書き、 **[atpages.jp](http://atpages.jp/)** の **PHP5.3.3** の環境では `VCWD_RENAME` マクロによる上書きが行われているようであった。どうしてこんな中途半端なアップデートしたんだよ、やるならもうちょっと一貫性持たせろよ…

# **結論**

- ファイルを **[move_uploaded_file](http://www.php.net/manual/ja/function.move-uploaded-file.php)** 関数で移動させてもいいし、 **[copy](http://www.php.net/manual/ja/function.copy.php)** 関数でコピーしてもいい。
- 移動またはコピーにどちらの関数を用いたとしても、パーミッションを直後に **[chmod](http://www.php.net/manual/ja/function.chmod.php)** 関数によって定めておくのが一番無難である。 **[umask](http://www.php.net/manual/ja/function.umask.php)** 関数による設定方法はスクリプト全体に影響を及ぼす上に、上書き時の挙動に一貫性が無いので推奨されない。
- **[is_uploaded_file](http://www.php.net/manual/ja/function.is-uploaded-file.php)** 関数を使う必要性は全くない。