處理)
PHP 預(yù)處理與 SQL 注入一、 認(rèn)識 SQL 注入1. 什么是 SQL 注入SQL 注入是指攻擊者通過在應(yīng)用程序的輸入?yún)?shù)中拼接惡意的 SQL 片段使得后臺執(zhí)行的 SQL 語句偏離預(yù)期從而導(dǎo)致數(shù)據(jù)泄露、篡改或刪除。經(jīng)典漏洞示例直接拼接變量$id$_GET[id];// 假設(shè)攻擊者輸入: 1 OR 11$sqlSELECT * FROM users WHERE id .$id;// 最終執(zhí)行的 SQL: SELECT * FROM users WHERE id 1 OR 11// 結(jié)果整張表的數(shù)據(jù)全部被泄露2. 為什么傳統(tǒng)的轉(zhuǎn)義不夠安全早期人們常用addslashes()或mysql_real_escape_string()來轉(zhuǎn)義特殊字符如單引號。但這種方法依賴開發(fā)者的人工判斷且在復(fù)雜的查詢條件如ORDER BY、LIMIT后跟數(shù)字時不需要引號中極易被繞過維護(hù)成本高且不可靠。二、 預(yù)處理的核心理念預(yù)處理是現(xiàn)代數(shù)據(jù)庫驅(qū)動防范 SQL 注入的終極武器。1. 預(yù)處理的執(zhí)行機(jī)制預(yù)處理將 SQL 語句的執(zhí)行分為兩步預(yù)編譯先把 SQL 語句的“骨架”發(fā)送給數(shù)據(jù)庫引擎。此時不包含任何用戶數(shù)據(jù)數(shù)據(jù)庫只會對骨架進(jìn)行語法解析和執(zhí)行計劃優(yōu)化。傳參執(zhí)行將用戶輸入的數(shù)據(jù)作為“參數(shù)”發(fā)送給數(shù)據(jù)庫。數(shù)據(jù)庫會將這些參數(shù)**嚴(yán)格當(dāng)作純數(shù)據(jù)字面量**處理絕不會將其解析為 SQL 指令。2. 形象的比喻傳統(tǒng)拼接就像給一張紙條寫上“請把[用戶輸入]放進(jìn)保險箱”。如果用戶輸入“一百塊錢然后把保險箱炸了”整個動作就變了味。預(yù)處理像是一個固定格式的表單“收款人金額”。無論用戶在橫線上填什么銀行系統(tǒng)都只會把它當(dāng)作“名字”和“數(shù)字”絕不會把填寫的文字當(dāng)作指令執(zhí)行。三、 PDO 預(yù)處理實(shí)戰(zhàn)PDO (PHP Data Objects) 是 PHP 推薦的數(shù)據(jù)庫抽象層支持多種數(shù)據(jù)庫默認(rèn)使用預(yù)處理。1. 連接數(shù)據(jù)庫$host127.0.0.1;$dbtest_db;$userroot;$passpassword;$charsetutf8mb4;$dsnmysql:host$host;dbname$db;charset$charset;$options[PDO::ATTR_ERRMODEPDO::ERRMODE_EXCEPTION,// 開啟異常模式PDO::ATTR_DEFAULT_FETCH_MODEPDO::FETCH_ASSOC,// 默認(rèn)關(guān)聯(lián)數(shù)組獲取PDO::ATTR_EMULATE_PREPARESfalse,// 關(guān)閉模擬預(yù)處理強(qiáng)制使用數(shù)據(jù)庫原生預(yù)處理極其重要];try{$pdonewPDO($dsn,$user,$pass,$options);}catch(\PDOException$e){thrownew\PDOException($e-getMessage(),(int)$e-getCode());}** 注意**PDO::ATTR_EMULATE_PREPARES false非常關(guān)鍵。如果為 true某些舊版本默認(rèn)PDO 會在本地進(jìn)行字符串拼接模擬預(yù)處理存在邊緣情況下的注入風(fēng)險。設(shè)為 false 則完全交由數(shù)據(jù)庫底層處理。2. 增刪改查CRUD示例【插入數(shù)據(jù)】 - 命名參數(shù)法推薦$username$_POST[username];$email$_POST[email];// 1. 準(zhǔn)備骨架使用 :name 作為占位符$sqlINSERT INTO users (username, email) VALUES (:username, :email);$stmt$pdo-prepare($sql);// 2. 綁定并執(zhí)行// execute 接收一個關(guān)聯(lián)數(shù)組鍵名必須與占位符一致$stmt-execute([:username$username,:email$email]);echo新用戶ID為: .$pdo-lastInsertId();【查詢數(shù)據(jù)】 - 問號參數(shù)法$id$_GET[id];// 1. 準(zhǔn)備骨架使用 ? 作為占位符$sqlSELECT * FROM users WHERE id ?;$stmt$pdo-prepare($sql);// 2. 綁定并執(zhí)行// execute 接收一個索引數(shù)組按順序?qū)?yīng) ?$stmt-execute([$id]);// 3. 獲取結(jié)果$user$stmt-fetch();print_r($user);【更新數(shù)據(jù)】 - 使用bindParam綁定變量$id5;$newName李四;$sqlUPDATE users SET username ? WHERE id ?;$stmt$pdo-prepare($sql);// bindParam 綁定的是變量引用第三個參數(shù)可指定類型更安全$stmt-bindParam(1,$newName,PDO::PARAM_STR);$stmt-bindParam(2,$id,PDO::PARAM_INT);$stmt-execute();echo影響了 .$stmt-rowCount(). 行;四、 MySQLi 預(yù)處理實(shí)戰(zhàn)MySQLi 是 PHP 針對 MySQL 數(shù)據(jù)庫的專用擴(kuò)展性能略優(yōu)于 PDO但僅支持 MySQL。1. 連接數(shù)據(jù)庫$mysqlinewmysqli(127.0.0.1,root,password,test_db);if($mysqli-connect_errno){die(連接失敗: .$mysqli-connect_error);}$mysqli-set_charset(utf8mb4);// 設(shè)置字符集2. 增刪改查CRUD示例注意MySQLi不支持命名參數(shù)如:name只支持問號占位符?。【插入數(shù)據(jù)】$username$_POST[username];$email$_POST[email];$sqlINSERT INTO users (username, email) VALUES (?, ?);$stmt$mysqli-prepare($sql);// bind_param(類型字符串, 變量1, 變量2...)// 類型i int, d double, s string, b blob$stmt-bind_param(ss,$username,$email);if($stmt-execute()){echo插入成功ID: .$mysqli-insert_id;}$stmt-close();【查詢數(shù)據(jù)】$id$_GET[id];$sqlSELECT id, username, email FROM users WHERE id ?;$stmt$mysqli-prepare($sql);// 假設(shè) id 是整數(shù)$stmt-bind_param(i,$id);$stmt-execute();// 獲取結(jié)果集$result$stmt-get_result();while($row$result-fetch_assoc()){print_r($row);}$stmt-close();五、 預(yù)處理的“盲區(qū)”與進(jìn)階場景預(yù)處理雖然能防住 99% 的注入但它不能防范所有情況。以下場景需要額外處理1. 不能預(yù)處理表名和列名預(yù)處理只能用于綁定數(shù)據(jù)值WHERE、VALUES 后面的值不能用于綁定 SQL 關(guān)鍵字、表名或列名。// 錯誤示范這是無效的會導(dǎo)致語法錯誤$sqlSELECT * FROM ? WHERE id ?;$stmt$pdo-prepare($sql);$stmt-execute([users,1]);// 正確做法表名/列名使用白名單校驗(yàn)$allowed_tables[users,admins,guests];$table$_GET[table];if(!in_array($table,$allowed_tables)){die(非法數(shù)據(jù)表);}$sqlSELECT * FROM{$table}WHERE id ?;$stmt$pdo-prepare($sql);$stmt-execute([1]);2. LIKE 模糊查詢的預(yù)處理在 LIKE 查詢中%和_是通配符。如果用戶輸入了這些符號雖然不會導(dǎo)致 SQL 注入但會導(dǎo)致查詢邏輯異常。需要正確拼接%。$keyword$_GET[keyword];// 正確做法將 % 放在 PHP 側(cè)拼接作為整體參數(shù)傳入$sqlSELECT * FROM users WHERE username LIKE ?;$stmt$pdo-prepare($sql);$searchTerm%{$keyword}%;$stmt-execute([$searchTerm]);// 如果需要防止通配符濫用可以使用 addcslashes 轉(zhuǎn)義// $keyword addcslashes($keyword, %_);3. IN 子句的預(yù)處理預(yù)處理的一個占位符?只能代表一個值。如果IN語句中有動態(tài)數(shù)量的參數(shù)需要動態(tài)生成占位符。$ids[1,2,3,4];// 來自用戶的數(shù)組// 動態(tài)生成同等數(shù)量的占位符 ?,?,?,?$placeholdersimplode(,,array_fill(0,count($ids),?));$sqlSELECT * FROM users WHERE id IN ($placeholders);$stmt$pdo-prepare($sql);// 將數(shù)組作為參數(shù)傳入$stmt-execute($ids);$results$stmt-fetchAll();六、 PDO 與 MySQLi 預(yù)處理對比總結(jié)特性PDOMySQLi (面向?qū)ο?支持?jǐn)?shù)據(jù)庫12 種僅 MySQL占位符語法?和:name均支持僅支持?參數(shù)綁定方式execute(array)或bindParam()bind_param(類型, 變量)類型安全可選指定 (PDO::PARAM_INT等)強(qiáng)制指定獲取結(jié)果集fetch()/fetchAll()get_result()-fetch_assoc()模擬預(yù)處理默認(rèn)開啟(建議關(guān)閉)無此概念全為原生