$_SERVER['HTTP_REFERER'] 是判斷請求來源的常用手段,但極不可靠。本文分析其局限性,提供防盜鏈、CSRF 防護的正確實作方案。
| 特性 | 說明 |
|---|---|
| 來源 | 瀏覽器自動發送 (非強制) |
| 可偽造 | 極易 (curl -H "Referer: ...", 瀏覽器擴充套件) |
| 可為空 | 直接輸入網址、書籤、HTTPS→HTTP、隱私模式、安全軟體 |
| 可被移除 | <meta name="referrer" content="no-referrer">, Referrer-Policy |
| 跨域限制 | 預設同源才發送,跨域需 Referrer-Policy: unsafe-url |
<?php
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if ($referer) {
$parsed = parse_url($referer);
$host = $parsed['host'] ?? '';
// 簡易同源檢查
$allowedHosts = ['example.com', 'www.example.com', 'api.example.com'];
if (in_array($host, $allowedHosts)) {
echo "來源可信: $host";
} else {
echo "外部來源: $host";
}
} else {
echo "無 Referer (直接訪問/隱私模式/被移除)";
}
?>
<?php
// 防盜鏈核心:簽名 URL,而非信任 Referer
class HotlinkProtector {
private string $secret;
private int $ttl; // 秒
public function __construct(string $secret, int $ttl = 3600) {
$this->secret = $secret;
$this->ttl = $ttl;
}
// 產生受保護 URL
public function signUrl(string $path, array $params = []): string {
$params['expires'] = time() + $this->ttl;
$params['signature'] = $this->generateSignature($path, $params);
return $path . '?' . http_build_query($params);
}
// 驗證請求
public function validate(string $path, array $query): bool {
if (!isset($query['expires'], $query['signature'])) return false;
if ($query['expires'] < time()) return false; // 過期
$expected = $this->generateSignature($path, $query);
return hash_equals($expected, $query['signature']);
}
private function generateSignature(string $path, array $params): string {
$data = $path . '?' . http_build_query(array_diff_key($params, ['signature' => '']));
return hash_hmac('sha256', $data, $this->secret);
}
}
// 使用
$protector = new HotlinkProtector('your-secret-key');
// 產生圖片 URL (有效期 1 小時)
$imgUrl = $protector->signUrl('/images/photo.jpg', ['w' => 800]);
// 輸出: /images/photo.jpg?w=800&expires=1234567890&signature=abc123...
// 驗證端點
if (!$protector->validate($_SERVER['REQUEST_URI'], $_GET)) {
http_response_code(403);
exit('存取被拒絕:連結無效或已過期');
}
// 正常輸出圖片...
?>
<?php
// CSRF 核心:Session 中儲存 Token,表單提交時驗證
class CsrfGuard {
private const TOKEN_NAME = 'csrf_token';
private const HEADER_NAME = 'X-CSRF-Token';
public static function generate(): string {
if (empty($_SESSION[self::TOKEN_NAME])) {
$_SESSION[self::TOKEN_NAME] = bin2hex(random_bytes(32));
}
return $_SESSION[self::TOKEN_NAME];
}
public static function validate(string $token): bool {
if (empty($_SESSION[self::TOKEN_NAME]) || empty($token)) return false;
return hash_equals($_SESSION[self::TOKEN_NAME], $token);
}
public static function rotate(): string {
$_SESSION[self::TOKEN_NAME] = bin2hex(random_bytes(32));
return $_SESSION[self::TOKEN_NAME];
}
// 中介層自動驗證 (除 GET/HEAD/OPTIONS)
public static function middleware(): void {
if (in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD', 'OPTIONS'])) return;
$token = $_POST[self::TOKEN_NAME]
?? $_SERVER['HTTP_' . str_replace('-', '_', strtoupper(self::HEADER_NAME))]
?? '';
if (!self::validate($token)) {
http_response_code(419); // CSRF Token Mismatch
exit('CSRF 驗證失敗,請重新整理頁面');
}
}
}
// 表單加入 Token
// <input type="hidden" name="csrf_token" value="= CsrfGuard::generate() ?>">
// AJAX 設定 Header
// fetch('/api/data', { headers: { 'X-CSRF-Token': document.querySelector('[name=csrf_token]').value } })
?>
<!-- HTML meta 標籤 -->
<meta name="referrer" content="strict-origin-when-cross-origin">
<!-- 或 HTTP Header (Nginx/Apache/PHP) -->
<?php
// PHP 設定
header('Referrer-Policy: strict-origin-when-cross-origin');
?>
# Nginx
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Apache
Header always set Referrer-Policy "strict-origin-when-cross-origin"
| 值 | 行為 | 推薦場景 |
|---|---|---|
| no-referrer | 完全不發送 | 高隱私需求 |
| strict-origin-when-cross-origin | 同源完整 URL,跨域僅 Origin,HTTPS→HTTP 不發送 | 推薦預設 |
| origin-when-cross-origin | 同源完整 URL,跨域僅 Origin | 需跨域 Origin |
| unsafe-url | 永遠發送完整 URL | 極少數分析需求 |
核心原則:HTTP_REFERER 僅供參考,安全相關決策必須使用加密簽名、CSRF Token、JWT 等可驗證機制。