ThinkPHP 作為國產框架代表,其安全機制備受關注。本文深入源碼剖析 TP 3.2.3 的核心安全體系:I() 函數輸入過濾、think_filter() 特殊字符處理、模型層與驅動層的 SQL 注入防禦多層架構,揭示框架如何在性能與安全間取得平衡。
I() 是 TP 統一的輸入獲取介面,支援 get.、post.、put.、param. 等多種格式,核心邏輯分三層:
function I($name, $default = '', $filter = null, $datas = null) {
// 解析修飾符:I('post.name')、I('get.') 等
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name, 2);
} elseif (C('VAR_AUTO_STRING')) {
$type = 's'; // 默認強制轉字符串
}
// 解析數據源
if (strpos($name, '.')) {
list($method, $name) = explode('.', $name, 2);
} else {
$method = 'param'; // 自動判斷
}
switch (strtolower($method)) {
case 'get': $input = &$_GET; break;
case 'post': $input = &$_POST; break;
case 'put': /* 解析 php://input */ break;
case 'param': /* 合並 $_GET + $_POST */ break;
case 'path': /* PATH_INFO 解析 */ break;
}
// ...
// 獲取全部變量
if ('' === $name) {
$data = $input;
$filters = isset($filter) ? $filter : C('DEFAULT_FILTER');
if ($filters) {
is_string($filters) && $filters = explode(',', $filters);
foreach ($filters as $filter) {
$data = array_map_recursive($filter, $data); // 遞歸處理多維數組
}
}
}
// 獲取單個變量
elseif (isset($input[$name])) {
$data = $input[$name];
$filters = isset($filter) ? $filter : C('DEFAULT_FILTER');
if ($filters) {
// 支援正則驗證、回調函數、類方法等多種過濾方式
// ...
}
// 類型強制轉換
if (!empty($type)) {
switch (strtolower($type)) {
case 'a': $data = (array)$data; break;
case 'd': $data = (int)$data; break;
case 'f': $data = (float)$data; break;
case 'b': $data = (bool)$data; break;
case 's': default: $data = (string)$data;
}
}
}
// 3.2.3 新增,位於 Common/functions.php
function think_filter(&$value) {
// 過濾 SQL 關鍵字作為鍵名時的注入風險
if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
$value .= ' '; // 關鍵字後加空格,破壞語法結構
}
}
// 調用點:I() 函數返回前
is_array($data) && array_walk_recursive($data, 'think_filter');
TP 支援陣列格式構建查詢條件:
$map['id'] = ['in', '1,2,3']; // 正常:WHERE id IN (1,2,3) $map['id'] = ['exp', ' IN (1,2,3) ']; // 危險:直接拼接 SQL 片段
攻擊向量:
// 攻擊者提交:id[in]=1&id[exp]=OR 1=1 // 若無過濾,將構成:WHERE id IN (1) OR 1=1
// 經過 think_filter 處理後: $data['id'] = ['in ' => '1,2,3', 'exp ' => ' OR 1=1 ']; // 鍵名變為 'in '、'exp ',無法匹配內部 exp 驗證邏輯,直接當作普通字段處理
優雅之處:不阻攔正常業務,僅在「關鍵字作為鍵名」這一特定攻擊面上做最小干預。
public function where($where, $parse = null) {
// 字符串格式:where('id=%d AND name=%s', [$id, $name])
if (!is_null($parse) && is_string($where)) {
if (!is_array($parse)) $parse = array_slice(func_get_args(), 1);
$parse = array_map([$this->db, 'escapeString'], $parse); // escapeString 內部調用 mysql_real_escape_string
$where = vsprintf($where, $parse);
}
// 物件轉陣列
elseif (is_object($where)) $where = get_object_vars($where);
// 字符串條件包裝為 _string 格式
if (is_string($where) && '' !== $where) {
$where = ['_string' => $where];
}
// 合併到 options
$this->options['where'] = isset($this->options['where'])
? array_merge($this->options['where'], $where)
: $where;
return $this;
}
protected function _parseOptions($options = []) {
// ... 合併 options ...
// 字段白名單檢查:僅允許數據表存在的字段進入查詢條件
if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
foreach ($options['where'] as $key => $val) {
$key = trim($key);
if (in_array($key, $fields, true)) {
// 字段存在:類型檢查與強制轉換
if (is_scalar($val)) $this->_parseType($options['where'], $key);
} elseif (!is_numeric($key) && '_' !== $key[0] &&
strpos($key, '.') === false && strpos($key, '(') === false &&
strpos($key, '|') === false && strpos($key, '&') === false) {
// 非法字段:嚴格模式拋異常,非嚴格模式直接丟棄
if (!empty($this->options['strict'])) {
E(L('_ERROR_QUERY_EXPRESS_') . ':[' . $key . '=>' . $val . ']');
}
unset($options['where'][$key]);
}
}
}
// ...
}
最終 SQL 組裝發生在 Db/Driver.class.php:
protected function parseWhere($where) {
// ... 邏輯運算符處理 ...
foreach ($where as $key => $val) {
if (0 === strpos($key, '_')) {
// _string, _complex 等特殊格式
$whereStr .= $this->parseThinkWhere($key, $val);
} else {
// 普通字段:parseKey 防注入轉義字段名
$whereStr .= $this->parseWhereItem($this->parseKey($key), $val);
}
}
// ...
}
protected function parseWhereItem($key, $val) {
if (is_array($val)) {
$exp = strtolower($val[0]);
// 比較操作:eq, neq, gt, egt, lt, elt
if (preg_match('/^(eq|neq|gt|egt|lt|elt)$/', $exp)) {
return $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]);
}
// 模糊查詢:like, notlike
elseif (preg_match('/^(notlike|like)$/', $exp)) {
// 支援陣列值自動 OR 組合
}
// IN / NOT IN
elseif (preg_match('/^(notin|not in|in)$/', $exp)) {
$zone = implode(',', $this->parseValue($val[1]));
return $key . ' ' . $this->exp[$exp] . ' (' . $zone . ')';
}
// BETWEEN
elseif (preg_match('/^(notbetween|not between|between)$/', $exp)) {
// ...
}
// exp 直接拼接 (高風險,需開發者自負責任)
elseif ('exp' === $exp) {
return $key . ' ' . $val[1];
}
} else {
// 純量值:字符串字段默認 LIKE,其它 =
$likeFields = $this->config['db_like_fields'];
if ($likeFields && preg_match('/^(' . $likeFields . ')$/i', $key)) {
return $key . ' LIKE ' . $this->parseValue('%' . $val . '%');
}
return $key . ' = ' . $this->parseValue($val);
}
}
protected function parseValue($value) {
if (is_string($value)) {
return $this->db->escapeString($value); // 核心:addslashes / mysql_real_escape_string
}
elseif (is_array($value)) {
return array_map([$this, 'parseValue'], $value);
}
elseif (is_bool($value)) {
return $value ? '1' : '0';
}
elseif (is_null($value)) {
return 'NULL';
}
return $value;
}
| 層級 | 組件 | 核心手段 | 防禦目標 |
|---|---|---|---|
| 輸入層 | I() + think_filter | DEFAULT_FILTER(htmlspecialchars) + 關鍵字鍵名空格化 | XSS、陣列鍵名注入 |
| 模型層 | where() + _parseOptions() | 字段白名單 + 類型強制轉換 + escapeString | 非法字段注入、類型混淆 |
| 驅動層 | parseWhere() + parseValue() | 統一值轉義 + 操作符白名單 + 字段名轉義 | SQL 語法注入、值型注入 |
mysql_real_escape_string (需已連接 DB),否則回退 addslashes。非 UTF-8 編碼環境下 addslashes 存在寬字節注入風險。建議:強制項目使用 UTF-8,或升級至 PDO 預處理。['exp', 'SQL 片段'] 繞過所有過濾,直接拼接。建議:代碼審計時重點檢查 exp 用法,嚴禁用戶輸入直接進入 exp。where('id = 1 OR 1=1') 直接進入 SQL。建議:禁用字符串條件,統一使用陣列格式。htmlspecialchars_decode。建議:存入原始數據,輸出時按上下文編碼 (HTML/JS/URL)。ThinkPHP 3.2.3 構建了「輸入過濾 → 模型白名單 → 驅動層轉義」三層立體防禦體系,think_filter 的空格破壞法思路獨特且高效。雖然存在歷史包袱 (addslashes、exp 直通),但在正確配置 (UTF-8、嚴格模式、禁用高危特性) 下,足以應對絕大多數 Web 攻擊。理解這套機制,有助於在遷移至 ThinkPHP 5/6、Laravel 等現代框架時,快速定位對應安全特性並做平滑過渡。