move_uploaded_file()
does not work if I create a file using tempnam
.
I am using Yii2's UploadedFile::getInstance()
method to create a file instance. Now, the difference here is the fact that I am populating $_FILES
like the following, before making the instance:
function addToFiles($key, $model, $url) {
$tempName = tempnam(ini_get('upload_tmp_dir'), 'php_files');
$originalName = basename(parse_url($url, PHP_URL_PATH));
$curl = new \common\components\Curl();
$imgRawData = $curl->get($url);
$fp = fopen($tempName, 'w');
fwrite($fp, $imgRawData);
fclose($fp);
$_FILES[$model] = array(
'name' => [$key => $originalName],
'type' => [$key => 'image/jpeg'],
'tmp_name' => [$key => $tempName],
'error' => [$key => 0],
'size' => [$key => strlen($imgRawData)],
);
}
Then, I create an instance like following:
$file = UploadedFile::getInstance($instance, $attr);
Dump of the $file
:
yii\web\UploadedFile#1
(
[name] => '20421_33_t.jpg'
[tempName] => 'D:\\xampp\\tmp\\php8005.tmp'
[type] => 'image/jpeg'
[size] => 2527
[error] => 0
)
Now, if I call:
$file->saveAs($folder . '/' . $filename);
It just returns false
, without any warning or exception.
Now, I did some digging around and found out that Yii uses move_uploaded_file()
(without any error suppression operator) in saveAs()
method. The following is the core saveAs()
method.
public function saveAs($file, $deleteTempFile = true)
{
if ($this->error == UPLOAD_ERR_OK) {
if ($deleteTempFile) {
return move_uploaded_file($this->tempName, $file);
} elseif (is_uploaded_file($this->tempName)) {
return copy($this->tempName, $file);
}
}
return false;
}
As per move_uploaded_file()
documentation a warning should have been issued when return is false, but I get nothing! Error reporting + Yii2's debugger is enabled in my setup and it should break out on slightest of the error!
Question is- Is this by design? Or is this a bug? Or perhaps I am doing something horribly wrong?
By the way, I am not really looking for an alternative solution because I have it already. I am curious to find out why this does not work, more than anything.
Things to note:
$folder
is writable. is_dir($folder) && is_writable($folder)
returns true.
$file->tempName
exists and not readonly! readfile($file->tempName)
reads the file without a problem.
I even tried chmod($file->tempName , 0777);
but it doesn't work.
The same method works perfectly if I do not use addToFiles
method, and upload the same image file from the form. In such cases, the dump of the instance is exactly the same ( only the tempName is different obviously, e.g. D:\\xampp\\tmp\\phpDBCC.tmp
), but the file uploads.
This is expected and you need to find some other way.
Issue one: You should not modify $_*
super globals. There are different things which can break. The is code which expects them to be immutable. If you're lucky you can get through this, but don't change it, but find a way to abstract from those files and mock the abstraction.
Issue two: move_uploaded_files()
is a special purpose function. The purpose is to bypass security measurements (like open_basedir or, historic, safe_mode) to access specific files owned by PHP's internals. This is needed as the uploaded file is received from the client before PHP can handle the request and properly identify the script to run. This function is not supposed to be used with other files except those created by PHP on the start of the specific request.
To provide a proper solution you have to elaborate what you actually want to achieve. My guess is that you want to get data from somewhere else and don't want to have a broken file. or that purpose do something like this:
$tempName = tempnam($folder, 'php_files');
/* .... */
$fp = fopen($tempName, 'w');
/* ... */
fclose($fp);
rename($tempName, $folder . '/' . $filename);
Mind that I directly write to $folder
. This ensures that we're using the same device thus we while storing the file we see that we have the needed rights and enough disk space. The rename()
then is very fast as we're on the same device.
This still isn't perfect, as the script might crash in between for any reason. This would leave the temporary file there. So you'd need a cleanup routine.
PHP has rename(), a function that can (as its name suggests) move files. Yes, I was being ironic, the name sucks.
It's not that PHP, as any other organically evolved creature, doesn't have duplicate features, but the whole purpose of having move_uploaded_files() is being able to not move certain files; more specifically, those that happened to be in the requested path but were never uploaded by the user, e.g.:
move_uploaded_files('/etc/passwd', '/var/www/html/img'); // false!
I honestly don't have the faintest idea of the attack vector this function tries to fight but I'm not a security expert and the function functionality is clear.
As about the lack of warnings, it's just as documented. Let's read carefully (emphasis is mine):
Return Values
Returns
TRUE
on success.If filename is not a valid upload file, then no action will occur, and move_uploaded_file() will return
FALSE
.If filename is a valid upload file, but cannot be moved for some reason, no action will occur, and move_uploaded_file() will return
FALSE
. Additionally, a warning will be issued.
The rationale? Perhaps they thought that using move_uploaded_file()
to move a file that was not uploaded in the fist place did not need further explanation but a system error like disk full or permission denied could use some details.
I can think of perfectly valid scenarios where you may want to fake an upload (unit testing comes to my mind) but that should be based in an entirely redesigned architecture with stubs, mocks and all that fancy stuff, where move_uploaded_files() is possibly the least of your worries.