安全不是功能,是過程。本文系統整理 PHP 應用程式開發的七大核心安全習慣,涵蓋輸入、檔案系統、資料庫、Session、XSS、表單偽造、CSRF 七大防線,並提供可直接落地的程式碼模式。
<?php
// ❌ 黑名單:嘗試過濾壞東西 (永遠不完整)
function badValidate($input) {
return str_replace(['<script>', 'DROP TABLE', '--'], '', $input);
}
// ✅ 白名單:只接受好東西
function validateUsername(string $input): ?string {
// 只允許字母數字底線,3-20 字元
if (preg_match('/^[a-zA-Z0-9_]{3,20}$/', $input)) {
return $input;
}
return null;
}
function validateEmail(string $input): ?string {
if (filter_var($input, FILTER_VALIDATE_EMAIL)) {
return $input;
}
return null;
}
function validateInt(string $input, int $min = 0, int $max = PHP_INT_MAX): ?int {
$val = filter_var($input, FILTER_VALIDATE_INT, ['options' => ['min_range' => $min, 'max_range' => $max]]);
return $val !== false ? $val : null;
}
// 使用:控制器層統一驗證
$username = validateUsername($_POST['username'] ?? '');
$email = validateEmail($_POST['email'] ?? '');
$age = validateInt($_POST['age'] ?? '', 18, 120);
if ($username === null || $email === null || $age === null) {
throw new InvalidArgumentException('輸入驗證失敗');
}
filter_var() 內建過濾器 (EMAIL, URL, IP, INT, FLOAT, BOOLEAN, REGEXP)filter_input() 直接從超全域變數取得並驗證ctype_*() 字符類型檢查 (alnum, alpha, digit, xdigit...)mb_check_encoding() 確保編碼合法<?php
// ❌ 危險:直接使用用戶輸入路徑
readfile($_GET['file']);
// ✅ 安全:基準目錄 + 實際路徑比對
function safeReadFile(string $baseDir, string $userInput): void {
$baseDir = realpath($baseDir);
$requestedPath = realpath($baseDir . DIRECTORY_SEPARATOR . $userInput);
// 確保請求路徑在基準目錄內
if ($requestedPath === false || strpos($requestedPath, $baseDir) !== 0) {
throw new RuntimeException('存取被拒絕');
}
// 可選:限制副檔名
$allowedExt = ['pdf', 'jpg', 'png', 'txt'];
$ext = strtolower(pathinfo($requestedPath, PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExt)) {
throw new RuntimeException('檔案類型不允許');
}
// 安全輸出
header('Content-Type: ' . mime_content_type($requestedPath));
header('Content-Disposition: inline; filename="' . basename($requestedPath) . '"');
readfile($requestedPath);
}
<?php
// ❌ 危險:字串拼接
$sql = "SELECT * FROM users WHERE name = '$name'";
// ✅ PDO 參數綁定
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = :name');
$stmt->execute([':name' => $name]);
$user = $stmt->fetch();
// ✅ 多條件動態查詢建構器
function buildWhere(array $filters, array $allowedFields): array {
$clauses = [];
$params = [];
foreach ($filters as $field => $value) {
if (!in_array($field, $allowedFields)) continue;
$clauses[] = "$field = :$field";
$params[":$field"] = $value;
}
return [implode(' AND ', $clauses), $params];
}
[$where, $params] = buildWhere($_GET, ['status', 'category', 'author']);
$sql = "SELECT * FROM posts" . ($where ? " WHERE $where" : "");
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
<?php
// 啟動前配置 (必須在 session_start() 前)
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // 僅 HTTPS
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', 1);
ini_set('session.gc_maxlifetime', 1800); // 30 分鐘
ini_set('session.sid_length', 48);
ini_set('session.sid_bits_per_character', 6);
// 自訂儲存處理器 (Redis 範例)
class RedisSessionHandler implements SessionHandlerInterface {
private $redis, $prefix = 'sess:', $ttl = 1800;
public function __construct(Redis $redis) { $this->redis = $redis; }
public function open($path, $name) { return true; }
public function close() { return true; }
public function read($id) { return $this->redis->get($this->prefix . $id) ?: ''; }
public function write($id, $data) { return $this->redis->setex($this->prefix . $id, $this->ttl, $data); }
public function destroy($id) { return $this->redis->del($this->prefix . $id); }
public function gc($maxlifetime) { return true; } // Redis 自動過期
}
session_set_save_handler(new RedisSessionHandler($redis), true);
session_start();
// 寫入前加密
$_SESSION['sensitive'] = sodium_crypto_secretbox(
json_encode($data),
random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES),
$key // 從環境變數/金鑰管理系統取得
);
// 讀取時解密
$data = json_decode(sodium_crypto_secretbox_open(
$_SESSION['sensitive'],
$nonce,
$key
), true);
| 輸出語境 | 編碼函數 | 範例 |
|---|---|---|
| HTML 內容 | htmlspecialchars($str, ENT_QUOTES, 'UTF-8') | <div>{$safe}</div> |
| HTML 屬性 | htmlspecialchars($str, ENT_QUOTES, 'UTF-8') | <input value="{$safe}"> |
| JavaScript 字串 | json_encode($str) (含引號) | const msg = {$safe}; |
| URL 參數 | rawurlencode($str) | <a href="?q={$safe}"> |
| CSS 值 | preg_replace('/[^a-zA-Z0-9_-]/', '', $str) | color: {$safe}; |
{-- Blade (Laravel) --}
<div>{{ $userInput }}</div> {{-- 自動 htmlspecialchars --}}
<div>{!! $trustedHtml !!}</div> {{-- 故意不逃逸,極謹慎 --}}
{# Twig (Symfony) #}
<div>{{ userInput }}{# 自動逃逸 #}
<div>{{ trustedHtml|raw }}{# 故意不逃逸 #}
header("Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';");
<?php
// 產生 Token (Session 儲存)
function generateCsrfToken(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// 驗證 Token
function verifyCsrfToken(string $token): bool {
if (empty($_SESSION['csrf_token'])) return false;
return hash_equals($_SESSION['csrf_token'], $token);
}
// 表單中嵌入
echo '<input type="hidden" name="csrf_token" value="' . generateCsrfToken() . '">';
// 處理端驗證
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
http_response_code(403);
exit('CSRF Token 無效');
}
// 處理業務邏輯...
}
SameSite: Lax (預設) 可阻擋大部分跨站 POST 請求攜帶 Cookie。關鍵操作 (刪除、轉帳) 仍需 CSRF Token 雙重保障。
受害者登入 A 網站 → 瀏覽惡意 B 網站 → B 網站自動提交表單到 A 網站 → 瀏覽器攜帶 A 的 Cookie → A 執行操作。
Lax 或 StrictX-Requested-With: XMLHttpRequest (簡單但可繞過)// Laravel: 所有非 GET/HEAD 路由自動驗證 (VerifyCsrfToken 中介軟體)
// Blade: @csrf 指令自動生成隱藏欄位
// Symfony: CsrfTokenManager + 表單類型自動整合
$form = $this->createFormBuilder()
->add('task', TextType::class)
->getForm(); // 自動加入 _token 欄位
| 習慣 | 核心動作 | 工具/函數 |
|---|---|---|
| 1. 輸入驗證 | 白名單、型別檢查、長度限制 | filter_var, preg_match, ctype_* |
| 2. 檔案系統 | 路徑比對、副檔名白名單、禁用執行 | realpath, finfo, Web Server 配置 |
| 3. 資料庫 | 參數化查詢、最小權限、錯誤隱藏 | PDO prepare, ORM |
| 4. Session | 安全 Cookie、Redis 儲存、敏感加密 | ini_set, SessionHandlerInterface, sodium |
| 5. XSS | 語境編碼、CSP、模板引擎自動逃逸 | htmlspecialchars, json_encode, CSP Header |
| 6. 表單偽造 | CSRF Token、SameSite Cookie | random_bytes, hash_equals |
| 7. CSRF | Token 驗證、SameSite、Referer 檢查 | 中介軟體、框架內建 |
安全是系統工程,非單點修補。將這七大習慣內化為團隊開發規範、代碼審查核心項目、CI/CD 自動化檢查 (SAST/DAST),才能構建真正可信賴的 PHP 應用。