2
\$\begingroup\$

I'm working on an PHP application that will allow (but not require) chunked file upload (from jQuery File Upload on the front side). I tried to find appropriate code and was only able to find the example from the JS library, that is, in my opinion, too bloated (it includes image resizing, etc).

I made my own, really basic, upload script that allows file upload, using chunked request or not (only one request).

<?php
define('MAX_FILE_SIZE_UPLOAD', 10000000); // 10Mb
header('Content-Type: application/json');

// Make sure file is not cached (as it happens for example on iOS devices)
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');

function validationFail($result, $title = 'Invalid request.', $code = 400) {
    header('HTTP/1.1 '.$code.' '.$title);
    echo(json_encode(array(
        'success' => false,
        'result'  => $result,
        'error'   => $title
    )));

    exit();
}

// We make sure no error occured
if (empty($_FILES['file']) || intval($_FILES['file']['error']) > 0) {
    validationFail('bad_request', 'An error occured.');
}

// We only allow CSV files. Of course, you can adapt this to your need
if ($_FILES['file']['type'] !== 'text/csv') {
    @unlink($_FILES['file']['tmp_name']);
    validationFail('bad_request', 'Please upload a CSV file.');
}

$tmpName = $_FILES['file']['tmp_name'];
$tmpDirectory = ini_get('upload_tmp_dir') ? ini_get('upload_tmp_dir') : sys_get_temp_dir();

// Security verification
if (!is_uploaded_file($tmpName)) {
    validationFail('bad_request', 'An error occured.');
}

// We start a session.
// This is required in order to track the sender of a file (not mix request)
if (session_status() == PHP_SESSION_NONE) {
    session_start();
}

if (isset($_SERVER['HTTP_CONTENT_RANGE']) && !empty($_SERVER['HTTP_CONTENT_RANGE']) && isset($_SERVER['CONTENT_LENGTH']) && is_numeric($_SERVER['CONTENT_LENGTH'])) {
    // We get the size of the file uploaded from the client (real, final size)
    $filesize = intval(substr($_SERVER['HTTP_CONTENT_RANGE'], strpos($_SERVER['HTTP_CONTENT_RANGE'], '/') + 1));

    if ($filesize > MAX_FILE_SIZE_UPLOAD) {
        validationFail('bad_request', 'File size allowed must be lower than 10Mb.');
    }

    if (isset($_SESSION['filename']) && is_file($tmpDirectory.'/'.$_SESSION['filename'])) {
        file_put_contents($tmpDirectory.'/'.$_SESSION['filename'], fopen($tmpName, 'r'), FILE_APPEND);
        unlink($tmpName); // We delete the file once we copied it, in order to not use unecessary storage

        // We stop here if the file is not completely loaded
        $currentSize = filesize($tmpDirectory.'/'.$_SESSION['filename']);
        if ($currentSize < $filesize) {
            exit('{"size": '.$currentSize.'}');
        } else {
            $tmpName = $tmpDirectory.'/'.$_SESSION['filename'];
        }
    } else {
        $_SESSION['filename'] = uniqid().'.csv.part';
        move_uploaded_file($tmpName, $tmpDirectory.'/'.$_SESSION['filename']);
        exit('{"size": '.filesize($tmpDirectory.'/'.$_SESSION['filename']).'}');
    }
}

// The file has been completely uploaded.
// You can do whatever you want now
// Like moving the file, which is located in $tmpName

// Note: don't use move_uploaded_file because the chuncked part created a new file and this function will fail.

rename($tmpName, dirname(__FILE__).'/files/'.uniqid().'.csv');
exit('{"success": true}');

I'd be curious to know your insight about it, mostly if there are any security issues I missed.

\$\endgroup\$
  • \$\begingroup\$ I'm using your script now. It's working great. However when a file gets uploaded that is smaller than the size of 1 chunk, it won't upload correctly. So i made a small extra check that processes the file directly when it's only 1 part. \$\endgroup\$ – David Ericsson Jun 21 '16 at 9:28
1
\$\begingroup\$

mostly if there are any security issues I missed.

Yes. $_FILES['file']['type'] is 100% user controlled. Because of this, an attacker can upload any files they want, including PHP files.

If this is actively exploitable depends on what upload_tmp_dir is (is it publicly accessible? then you are vulnerable), or what you do with the file at the end.

\$\endgroup\$
  • \$\begingroup\$ Indeed! But in the case of this code, you'll notice I move the file to some path and add ".csv" which should (if the server is well configured) not execute it as php code, but at worst, show the php code uploaded. \$\endgroup\$ – Cyril N. Jun 21 '16 at 11:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.