檔案系統是伺服器最核心資產。本文剖析路徑遍歷攻擊向量,示範脆弱代碼模式,提供多層次防禦策略與安全下載實作範本。
攻擊者透過控制檔名參數,使用 ../ 序列跳出預期目錄,讀取/寫入敏感檔案:
../../etc/passwd → 系統帳號資訊../.env → 應用機密金鑰../../var/www/session/sess_xxx → 竊取 Sessionphp://filter/convert.base64-encode/resource=config.php → 繞過副檔名限制讀取原始碼<?php
// 典型脆弱下載腳本
if ($_POST['submit'] === 'Download') {
$file = $_POST['fileName']; // 直接信任用戶輸入
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"$file\"");
$fh = fopen($file, 'r'); // 開啟任意路徑
while (!feof($fh)) echo fread($fh, 1024);
fclose($fh);
}
?>
風險點:無輸入驗證、無路徑正規化、無目錄限制、直接使用用戶輸入開啟檔案。
<?php
function isValidFileName($filename): bool {
// 僅允許英數、底線、連字號、點,禁止路徑分隔符
return (bool)preg_match('/^[a-zA-Z0-9_.-]+$/', $filename);
}
// 使用
$filename = $_POST['fileName'] ?? '';
if (!isValidFileName($filename)) {
die('非法檔名');
}
$safePath = "/var/www/uploads/$filename"; // 固定基礎目錄
?>
<?php
function resolveSafePath(string $baseDir, string $userInput): ?string {
// 1. 解析絕對路徑(解析 ..、.、符號連結)
$baseDir = realpath($baseDir);
$requested = realpath($baseDir . DIRECTORY_SEPARATOR . $userInput);
// 2. realpath 失敗(檔案不存在或連結斷裂)
if ($requested === false) return null;
// 3. 確保在基礎目錄內(防止路徑遍歷)
if (strpos($requested, $baseDir) !== 0) return null;
// 4. 可選:禁止符號連結跳出
// if (is_link($requested)) return null;
return $requested;
}
// 使用
$baseDir = '/var/www/uploads';
$safePath = resolveSafePath($baseDir, $_GET['file'] ?? '');
if (!$safePath || !is_file($safePath)) {
http_response_code(404);
die('檔案不存在');
}
?>
<?php
// 資料表 files: id, uuid, original_name, storage_path, mime, size, created_at
// 下載邏輯
$stmt = $pdo->prepare('SELECT storage_path, original_name, mime FROM files WHERE uuid = ? AND deleted_at IS NULL');
$stmt->execute([$_GET['id']]);
$file = $stmt->fetch();
if (!$file) {
http_response_code(404);
die('檔案不存在');
}
// 直接使用資料庫儲存的絕對路徑(已經過驗證)
$safePath = $file['storage_path'];
if (!is_file($safePath)) {
http_response_code(410);
die('檔案遺失');
}
// 安全下載
header('Content-Type: ' . $file['mime']);
header('Content-Disposition: attachment; filename="' . rawurlencode($file['original_name']) . '"');
header('Content-Length: ' . filesize($safePath));
readfile($safePath); // 比 fopen/fread 迴圈效率高
?>
<?php
/**
* 安全檔案下載控制器
*/
class SecureDownloadController {
private string $baseDir;
private array $allowedMimes = [
'application/pdf',
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'text/plain', 'text/csv',
'application/zip', 'application/x-zip-compressed',
];
public function __construct(string $baseDir) {
$this->baseDir = realpath($baseDir) ?: throw new InvalidArgumentException('基礎目錄不存在');
}
public function download(string $uuid): void {
// 1. 資料庫查詢(間接引用)
$file = $this->getFileRecord($uuid);
if (!$file) { $this->notFound(); return; }
// 2. 路徑安全性二次確認
$safePath = $this->resolvePath($file['storage_path']);
if (!$safePath || !is_file($safePath)) { $this->gone(); return; }
// 3. MIME 白名單(防止執行惡意腳本)
if (!in_array($file['mime'], $this->allowedMimes, true)) {
$this->forbidden(); return;
}
// 4. 輸出標頭
$this->sendHeaders($file, $safePath);
// 5. 串流輸出(大檔案不佔記憶體)
$this->streamFile($safePath);
}
private function resolvePath(string $path): ?string {
$real = realpath($path);
if (!$real || strpos($real, $this->baseDir) !== 0) return null;
return $real;
}
private function sendHeaders(array $file, string $path): void {
header('Content-Type: ' . $file['mime']);
header('Content-Disposition: attachment; filename="' . rawurlencode($file['original_name']) . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: private, max-age=0, must-revalidate');
header('Pragma: public');
}
private function streamFile(string $path): void {
$handle = fopen($path, 'rb');
while (!feof($handle)) {
echo fread($handle, 8192);
@ob_flush(); @flush();
}
fclose($handle);
}
private function getFileRecord(string $uuid): ?array { /* PDO 查詢 */ }
private function notFound(): void { http_response_code(404); exit; }
private function gone(): void { http_response_code(410); exit; }
private function forbidden(): void { http_response_code(403); exit; }
}
?>
open_basedir 限制 PHP 可存取路徑:open_basedir = /var/www/:/tmp/disable_functions 禁用高風險函數:exec,system,shell_exec,passthru,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_sourcelocation ~ \.php$ { deny all; })檔案系統安全的核心原則:不信任用戶輸入路徑、使用間接引用、多層驗證、最小權限原則。將這些實作封裝為專案共用元件,確保全站一致防護。