首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 檔案系統安全防護:路徑遍歷攻擊防禦與安全下載實作

     編輯:PHP 相關知識

守護伺服器檔案系統:從路徑遍歷原理到多層防禦實戰

檔案系統是伺服器最核心資產。本文剖析路徑遍歷攻擊向量,示範脆弱代碼模式,提供多層次防禦策略與安全下載實作範本。

攻擊原理:路徑遍歷

攻擊者透過控制檔名參數,使用 ../ 序列跳出預期目錄,讀取/寫入敏感檔案:

  • ../../etc/passwd → 系統帳號資訊
  • ../.env → 應用機密金鑰
  • ../../var/www/session/sess_xxx → 竊取 Session
  • php://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);
}
?>

風險點:無輸入驗證、無路徑正規化、無目錄限制、直接使用用戶輸入開啟檔案。

多層防禦策略

第 1 層:白名單驗證(最強)

<?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";  // 固定基礎目錄
?>

第 2 層:路徑正規化 + 目錄限制(深度防禦)

<?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('檔案不存在');
}
?>

第 3 層:資料庫映射間接引用(架構層面消除風險)

<?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_source
  • 上傳目錄禁用腳本執行(Nginx/Apache 設定 location ~ \.php$ { deny all; })
  • 檔案上傳重新命名(UUID + 副檔名驗證),不保留原始檔名
  • 定期掃描上傳目錄異常檔案(ClamAV、YARA 規則)

檔案系統安全的核心原則:不信任用戶輸入路徑、使用間接引用、多層驗證、最小權限原則。將這些實作封裝為專案共用元件,確保全站一致防護。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved