最近更新: 2014-12-19

PHP curl post file

日前跑一個 RESTful 服務舊案以 PHPUnit 設計的測試案例,發現有一些上傳檔案到 RESTful 服務的測試項目總是失敗。檢查程式後,發現問題出在測試案例的 curl 程式碼。

一直以來,用 PHP 的 curl 函數上傳檔案時,只需要啟用 CURLOPT_POST 選項,再於 CURLOPT_POSTFIELDS 的欄位值中,以 @ 標示檔案路徑, curl 就會自動幫我們處理讀取檔案內容並上傳給遠端的工作。參考 PHP 透過 HTTP POST 方法上傳資料與檔案給 RESTful 服務

但根據 PHP 線上使用手冊的記載,在 PHP 5.5 版之後,基於安全理由關閉了這項功能。要改用函數 curl_file_create() ,參考 PHP Manual: curl_file_create。原因很簡單,因為 curl 並不能區分你是真的要上傳檔案或是剛好輸入了一個開頭是 @ 字元的字串。如果你設計了一個用 curl POST 上傳資料的程式,又允許使用者輸入資料欄位,那麼駭客就可以故意填上 @/etc/passwd 讓 curl 將主機上的帳號清單傳出去。

像下面的程式碼範例,你設計欄位 field1 是一個字串欄位,讓使用者輸入文字,例如 hello world。若駭客抓住這一點,故意輸入 @/etc/passwd ,那麼 curl 就會認為這是要上傳檔案,而去讀取 /etc/passwd 的內容並將之送出。


$fields =array(
    'field1' => $input_string
);
$opts = array(
    CURLOPT_POST    => true,
    CURLOPT_POSTFIELDS => $fields
);

curl_setopt_array($hr, $opts);

為了修正這個安全問題, PHP 5.5 之後關閉了這個上傳檔案的捷徑(有組態項目可以開啟這功能,但基於安全理由,我不建議這麼做)。如果要上傳的表單欄位中包含檔案型態,那麼設計者必須明確地使用 curl_file_create() 函數處理這個資料欄位。當然,設計者有必要檢查檔案路徑是否安全。幸好這項調整工作很容易修改。如下所示:


$fields =array(
    // 'field1' => '@' . $file_path # INSECURITY!
    'field1' => curl_file_create($file_path) # Good.
);
$opts = array(
    CURLOPT_POST    => true,
    CURLOPT_POSTFIELDS => $fields
);

curl_setopt_array($hr, $opts);

對於 PHP 5.4 及其以前的舊版用戶,可以參考網友分享的偷懶替代法,在程式碼中加入下列定義。


# For PHP < 5.5:
# See also: http://php.net/manual/en/function.curl-file-create.php
if (!function_exists('curl_file_create')) {
    function curl_file_create($filename, $mimetype = '', $postname = '') {
        return "@$filename;filename="
            . ($postname ?: basename($filename))
            . ($mimetype ? ";type=$mimetype" : '');
    }
}

這個偷懶法就是定義一個 curl_file_create() ,但內容只是回傳一個以 @ 標示的字串。在舊版 PHP 中,它仍然會交給 curl 去自動載入檔案並上傳。但在新版 PHP 中,就會調用內建的 curl_file_create() 函數處理檔案的載入工作。

最後,提供一個無視 PHP 版本也不透過 curl 的方法,即下列的 do_post_request() 函數。


function do_post_request($url, $postdata = false, $files = false)
{
    $destination = $url;

    $eol = "\r\n";
    $data = '';

    $mime_boundary=md5(time());

    $data .= '--' . $mime_boundary . $eol;

    //Collect Postdata
    if ($postdata) {
        foreach($postdata as $key => $val) {
            $data .= "--$mime_boundary\n";
            $data .= "Content-Disposition: form-data; name="".$key.""\n\n".$val."\n";
        }
    }
    $data .= "--$mime_boundary\n";

    if ($files) {
        foreach ($files as $key => $content) {
            $data .= 'Content-Disposition: form-data; name="' .
                      $key . '"; filename="' . $key . '"' . $eol;
            $data .= 'Content-Type: application/octet-stream' . $eol;
            $data .= 'Content-Transfer-Encoding: binary' . $eol . $eol;
            $data .= $content . $eol;
        }
    }

    $data .= "--" . $mime_boundary . "--" . $eol . $eol; // finish with two eol's!!

    $params = array(
        'http' => array(
            'method' => 'POST',
            'header' => 'Content-Type: multipart/form-data; boundary=' .
                $mime_boundary . $eol,
            'content' => $data
        )
    );

    $ctx = stream_context_create($params);
    $response = @file_get_contents($destination, FILE_TEXT, $ctx);
    return $response;
}

不過這方法假設所有表單欄位都是檔案型態。如果你要上傳的表單混雜了一般文字欄位和檔案型態欄位的話,你要自己區分處理。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/35970791.html