首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

ThinkPHP 3.2.3 安全機制深度剖析:I 函數過濾、SQL 注入防禦與 think_filter 核心邏輯

     編輯:PHP 綜合知識

ThinkPHP 作為國產框架代表,其安全機制備受關注。本文深入源碼剖析 TP 3.2.3 的核心安全體系:I() 函數輸入過濾、think_filter() 特殊字符處理、模型層與驅動層的 SQL 注入防禦多層架構,揭示框架如何在性能與安全間取得平衡。

核心入口:I() 函數全景解析

I() 是 TP 統一的輸入獲取介面,支援 get.、post.、put.、param. 等多種格式,核心邏輯分三層:

1. 多源數據獲取

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;
    }
    // ...

2. 遞歸過濾機制

// 獲取全部變量
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. 終極防線:think_filter()

// 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');

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 驗證邏輯,直接當作普通字段處理

優雅之處:不阻攔正常業務,僅在「關鍵字作為鍵名」這一特定攻擊面上做最小干預。

模型層防禦:where() 與 _parseOptions()

where() 方法

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;
}

_parseOptions() 字段類型檢查與強制轉換

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]);
            }
        }
    }
    // ...
}

驅動層防禦:parseWhere() 與 parseValue()

最終 SQL 組裝發生在 Db/Driver.class.php:

parseWhere() 解析陣列條件

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);
        }
    }
    // ...
}

parseWhereItem() 處理各類比較操作

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);
    }
}

parseValue() 統一值轉義

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_filterDEFAULT_FILTER(htmlspecialchars) + 關鍵字鍵名空格化XSS、陣列鍵名注入
模型層where() + _parseOptions()字段白名單 + 類型強制轉換 + escapeString非法字段注入、類型混淆
驅動層parseWhere() + parseValue()統一值轉義 + 操作符白名單 + 字段名轉義SQL 語法注入、值型注入

遺留風險與改進建議

  1. addslashes vs mysql_real_escape_string:TP 優先使用 mysql_real_escape_string (需已連接 DB),否則回退 addslashes。非 UTF-8 編碼環境下 addslashes 存在寬字節注入風險。建議:強制項目使用 UTF-8,或升級至 PDO 預處理。
  2. exp 表達式直通車:['exp', 'SQL 片段'] 繞過所有過濾,直接拼接。建議:代碼審計時重點檢查 exp 用法,嚴禁用戶輸入直接進入 exp。
  3. _string 字符串條件:where('id = 1 OR 1=1') 直接進入 SQL。建議:禁用字符串條件,統一使用陣列格式。
  4. I() 默認 htmlspecialchars:防 XSS 有效,但導致數據庫存入實體字符,輸出時需 htmlspecialchars_decode。建議:存入原始數據,輸出時按上下文編碼 (HTML/JS/URL)。

總結

ThinkPHP 3.2.3 構建了「輸入過濾 → 模型白名單 → 驅動層轉義」三層立體防禦體系,think_filter 的空格破壞法思路獨特且高效。雖然存在歷史包袱 (addslashes、exp 直通),但在正確配置 (UTF-8、嚴格模式、禁用高危特性) 下,足以應對絕大多數 Web 攻擊。理解這套機制,有助於在遷移至 ThinkPHP 5/6、Laravel 等現代框架時,快速定位對應安全特性並做平滑過渡。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved