PHP 內建了多個安全相關函數,但隨著版本演進,部分函數已棄用、部分存在誤區、部分被更優方案取代。本文深度解析 8 個核心安全函數的原理、適用場景、常見誤用與現代替代方案,幫助開發者構建更安全的應用。
| 函數 | 用途 | 狀態 | 現代替代/補充 |
|---|---|---|---|
mysql_real_escape_string() |
SQL 轉義 | 🔴 已移除 (PHP 7.0+) | PDO 預處理語句 / 參數綁定 |
addslashes() |
通用轉義 | 🟡 不推薦 | 場景專用函數 / 預處理 |
htmlentities() |
HTML 實體編碼 | 🟢 推薦 (輸出編碼) | htmlspecialchars() 通常更合適 |
htmlspecialchars() |
HTML 特殊字符編碼 | 🟢 首選 (XSS 防護) | 配合 CSP 頭、模板引擎自動轉義 |
strip_tags() |
移除 HTML/JS/PHP 標籤 | 🟡 慎用 (非安全函數) | HTMLPurifier / 模板引擎沙箱 |
md5() |
雜湊 | 🔴 不可用於安全 | password_hash() / Argon2id / SHA-256+ |
sha1() |
雜湊 | 🔴 不可用於安全 | 同 md5 |
intval() |
整數轉換 | 🟢 推薦 (輸入驗證/類型安全) | (int) / filter_var() / 嚴格類型聲明 |
mysql_real_escape_string() — 歷史的遺物<?php
// ❌ 錯誤示範:已移除,PHP 7+ 會報 Fatal Error
$safe = mysql_real_escape_string($_POST['name'], $conn);
$query = "SELECT * FROM users WHERE name = '$safe'";
根本問題:依賴當前連接字符集,字符集設置不當導致繞過 (如 GBK 雙字節注入);且開發者極易忘記調用、重複調用、或在非字符串上下文使用。
現代標準:PDO 預處理語句
<?php
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // 關鍵:使用真實預處理
]);
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = ? AND status = ?');
$stmt->execute([$_POST['name'], 1]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
addslashes() — 萬金油陷阱<?php
// ❌ 錯誤:用於 SQL (不完整)、HTML (錯誤)、Shell (危險)
$safe = addslashes($_GET['input']);
// ❌ 錯誤:magic_quotes_gpc 開啟時會雙重轉義
if (get_magic_quotes_gpc()) { /* ... */ } // PHP 5.4+ 已移除
適用場景極少:僅用於「需要字面反斜杠的字符串字面量構造」極罕見情況。絕不用於 SQL、HTML、Shell、JSON、LDAP 等上下文。
htmlentities() vs htmlspecialchars() — XSS 防線<?php
// 完整參數簽名
htmlspecialchars(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, string $encoding = "UTF-8"): string
// 推薦調用 (PHP 8.1+ 默認即為此)
$safe = htmlspecialchars($userInput, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
| 標誌 | 作用 | 建議 |
|---|---|---|
ENT_QUOTES |
轉義單雙引號 | 必須開啟,防屬性注入 |
ENT_SUBSTITUTE |
無效編碼替換為 | 必須開啟,防編碼攻擊 |
ENT_HTML5 |
HTML5 規範實體表 | 現代項目推薦 |
核心原則:輸出時編碼,而非輸入時。數據庫存原始數據,模板渲染時自動轉義 (Twig/Blade/Latte/Smarty 皆內建)。
strip_tags() — 偽安全感<?php
// ❌ 繞過示例
$input = '
';
echo strip_tags($input, '
'); // 保留了 img,onerror 執行!
// ❌ 屬性注入
$input = 'click';
echo strip_tags($input, ''); // href 保留,點擊執行
strip_tags() 不是安全函數,它是格式化工具。無法防禦屬性事件、CSS 注入、編碼繞過。嚴禁用於安全過濾。需求「用戶提交富文本」請使用 HTMLPurifier 配合嚴格白名單。
md5() / sha1() — 密碼學已失效| 算法 | 碰撞攻擊 | 預像攻擊 | GPU 破解速度 (RTX 4090) |
|---|---|---|---|
| MD5 | 實用化 (2004) | 理論可行 | ~6000 億次/秒 |
| SHA-1 | 實用化 (2017, SHAttered) | 理論可行 | ~1800 億次/秒 |
| SHA-256 | 無已知 | 無已知 | ~30 億次/秒 |
| bcrypt (cost 12) | N/A | N/A | ~1.2 萬次/秒 |
| Argon2id | N/A | N/A | ~可調 (記憶體硬) |
密碼存儲黃金法則:
<?php
// 註冊/修改密碼
$hash = password_hash($password, PASSWORD_ARGON2ID); // PHP 7.2+
// 或兼容寫法
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// 驗證登入
if (password_verify($inputPassword, $hash)) {
// 成功
// 可選:檢查是否需要重新雜湊 (算法升級、cost 提高)
if (password_needs_rehash($hash, PASSWORD_ARGON2ID)) {
$newHash = password_hash($inputPassword, PASSWORD_ARGON2ID);
// 更新資料庫
}
}
password_hash() 自動生成鹽值、算法標識、成本參數,結果包含驗證所需一切VARCHAR(255) 足夠intval() / (int) — 類型安全基石<?php
// ✅ 正確:ID、分頁、排序等整數參數強制轉換
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = min(100, max(1, (int)($_GET['limit'] ?? 20)));
$userId = (int)($_POST['user_id'] ?? 0);
// ❌ 錯誤:用於浮點數金額
$amount = (int)($_POST['amount']); // 丟失小數!
// ✅ 更嚴格:filter_var 驗證範圍
$page = filter_var($_GET['page'] ?? 1, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'max_range' => 10000]
]) ?? 1;
配合 PHP 8.0+ 嚴格類型聲明 (declare(strict_types=1)) 與參數/返回類型提示,可在編譯期/運行期早期攔截類型錯誤。
Content-Security-Policy Header + htmlspecialchars() 手動輸出SameSite=Lax/Strict) + 雙重提交 Tokenpassword_hash() + password_verify() + 速率限制 + 多因子認證exec/shell_exec,改用 Symfony Process / 原生函數unserialize() 處理不可信數據,改用 JSONlibxml_disable_entity_loader(true) (PHP 8.0+ 默認禁用)PHP 安全函數經歷了「原生轉義 → 專用上下文函數 → 結構化分離 (預處理/模板引擎) → 語言級類型系統」的演進。現代 PHP 開發應遵循:
filter_var()、嚴格類型、DTO 驗證器password_hash() (密碼)、random_bytes() (Token)、hash_equals() (比對)棄用 mysql_real_escape_string、addslashes、strip_tags、md5、sha1 等歷史包袱,擁抱現代 PHP 安全生態,是構建可信應用的必要條件。