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

mysqli_set_charset vs SET NAMES:深度解析為何官方推薦前者

     編輯:PHP 綜合知識

前言:一個被忽視的安全細節

PHP 手冊明確建議:優先使用 mysqli_set_charset() 而非執行 SET NAMES SQL 語句。許多開發者知其然不知其所以然。本文從 C 源碼層面深度剖析兩者本質差異,揭示 SET NAMES 隱藏的字符集同步缺陷與注入風險。

SET NAMES 到底做了什麼?

SET NAMES utf8mb4
-- 等價於以下三條語句:
SET character_set_client = utf8mb4;
SET character_set_connection = utf8mb4;
SET character_set_results = utf8mb4;

這三個會話變量分別控制:客戶端發送編碼、連接層轉換編碼、結果集返回編碼。看似完美,實則只修改了 MySQL 服務端狀態。

mysqli_set_charset() 多做了關鍵一步

追踪 PHP 源碼 ext/mysqli/mysqli_nonapi.c → MySQL C API libmysql/client.c:

int mysql_set_character_set(MYSQL *mysql, const char *cs_name) {
    // 1. 獲取字符集資訊結構
    CHARSET_INFO *cs = get_charset_by_csname(cs_name, ...);

    // 2. 執行 SET NAMES
    char buff[64];
    sprintf(buff, "SET NAMES %s", cs_name);
    mysql_real_query(mysql, buff, strlen(buff));

    // 3. 【關鍵】更新客戶端 MYSQL 結構體的 charset 指針
    mysql->charset = cs;

    return 0;
}

為什麼 mysql->charset 這麼重要?

它直接影響 mysql_real_escape_string() 的轉義行為。

實驗:經典 GBK 雙字節注入(0x5c 反斜槓問題)

<?php
$link = mysqli_connect('localhost', 'root', 'pass', 'test', 3306);

// 字符 "縗" GBK 編碼:0x91 0x5c(第二字節恰是反斜槓)
$payload = "\x91\x5c"; 

// 1. 預設 latin1 連接
var_dump(mysqli_real_escape_string($link, $payload)); 
// 輸出: string(3) "縗\"  --> 0x5c 被轉義為 \0x5c,破壞字符邊界!

// 2. 執行 SET NAMES gbk
mysqli_query($link, "SET NAMES gbk");
var_dump(mysqli_real_escape_string($link, $payload)); 
// 輸出: string(3) "縗\"  --> 依然錯誤!因為 mysql->charset 仍指向 latin1

// 3. 使用 mysqli_set_charset
mysqli_set_charset($link, 'gbk');
var_dump(mysqli_real_escape_string($link, $payload)); 
// 輸出: string(2) "縗"   --> 正確!mysql->charset 已更新為 gbk,識別雙字節不轉義第二字節

核心差異總結表

維度SET NAMESmysqli_set_charset()
服務端字符集變量✅ 更新✅ 更新
客戶端 MYSQL 結構體 charset❌ 不變✅ 同步更新
mysql_real_escape_string 行為❌ 仍按舊字符集轉義✅ 按新字符集正確轉義
防注入可靠性低(特定多字節字符集可繞過)高
適用場景舊版兼容、非預處理查詢所有新代碼、安全敏感場景

PDO 對應方案

$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", 'user', 'pass');
// DSN 中 charset 參數會自動調用 mysql_set_character_set
// 等價於:
$pdo->exec("SET NAMES utf8mb4"); // 但 PDO 內部也同步了 charset 指針

最佳實踐建議

  1. 統一使用 mysqli_set_charset() 或 PDO DSN charset 設置連接字符集
  2. 拒絕手寫 SET NAMES,特別是 GBK、BIG5、SJIS 等多字節字符集環境
  3. 全面遷移至預處理語句(prepare()/bind_param()),從根源消除轉義依賴
  4. 資料庫、表、欄位、連接全鏈路統一 utf8mb4,避免隱式轉換
  5. 定期掃描代碼庫查找 SET NAMES、mysql_real_escape_string 殘留用法

延伸閱讀:C API 文檔原話

"mysql_set_character_set() is the preferred way to change the character set. Using SET NAMES does not update the charset member of the MYSQL structure, and thus does not affect the character set used by mysql_real_escape_string()."

總結

mysqli_set_charset() 不僅執行 SET NAMES,更關鍵地同步了客戶端 MYSQL 結構體的 charset 指針,確保 mysql_real_escape_string() 能根據正確字符集進行轉義,防範多字節字符集注入繞過。這是 C API 設計層面的安全保障,請務必在所有專案中強制使用。

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