直接暴露文件 URL 會導致熱鏈盜鏈、未授權下載、路徑遍歷攻擊。正確做法:PHP 代理讀取文件流並輸出,配合權限校驗、速率限制、斷點續傳。
<?php
function downloadFile(string $filePath, string $downloadName = '', array $options = []): bool {
// 1. 安全校驗:防止目錄遍歷
$realPath = realpath($filePath);
$allowedRoot = realpath(__DIR__ . '/protected_files');
if (!$realPath || strpos($realPath, $allowedRoot) !== 0 || !is_file($realPath)) {
http_response_code(404);
return false;
}
// 2. 文件名處理
$downloadName = $downloadName ?: basename($realPath);
$downloadName = rawurlencode($downloadName); // RFC 5987 兼容
// 3. 頭部設置
$fileSize = filesize($realPath);
$mimeType = mime_content_type($realPath) ?: 'application/octet-stream';
header('Content-Description: File Transfer');
header('Content-Type: ' . $mimeType);
header('Content-Disposition: attachment; filename="' . $downloadName . '"; filename*=UTF-8\'\'' . $downloadName);
header('Content-Length: ' . $fileSize);
header('Cache-Control: private, must-revalidate');
header('Pragma: public');
header('Expires: 0');
// 4. 可選:斷點續傳支援 (HTTP Range)
if (isset($_SERVER['HTTP_RANGE'])) {
// 解析 Range 頭,發送 206 Partial Content...
// 此處省略完整實現,建議使用成熟庫(如 Symfony HttpFoundation)
}
// 5. 分塊輸出,避免大文件佔滿記憶體
$handle = fopen($realPath, 'rb');
if ($handle) {
while (!feof($handle) && connection_status() === CONNECTION_NORMAL) {
echo fread($handle, 8192);
@ob_flush();
@flush();
}
fclose($handle);
}
return true;
}
// 使用範例
// downloadFile('/var/www/protected/report.pdf', 'Q3_Report.pdf');
?>
| 風險 | 對策 |
|---|---|
路徑遍歷 (../../etc/passwd) | realpath() + 根目錄前綴校驗 |
| 大文件記憶體溢出 | 分塊 fread + flush,禁用 readfile()/file_get_contents() |
| 並發連接耗盡 | Nginx X-Accel-Redirect / Apache X-Sendfile 交由 Web 服務器發送 |
| 未授權訪問 | 集成 ACL/RBAC 權限檢查、簽名 URL (JWT/Signed URL) |
| 惡意腳本執行 | 存儲目錄禁用 PHP 解析,Content-Disposition: attachment 強制下載 |
PHP 權限校驗 → 簽名 URL → Nginx X-Accel-Redirect 內部重定向 → 直接由 Nginx 發送文件,PHP 進程瞬間釋放,支援高併發大文件分發。