PHP 中的轉義函式小結
程式碼審計的時候經常會遇到種類繁雜的轉義函式,最可怕的是他們長的都很像,還是拿出來總結一下吧。
0X01 addslashes() –>(PHP 4, PHP 5, PHP 7)
用法:
string addslashes ( string $str )
返回值:
返回字串,該字串為了資料庫查詢語句等的需要在某些字元前加上了反斜線。這些字元是單引號(’)、雙引號(”)、反斜線(\)與 NUL(NULL 字元)。
一個使用 addslashes() 的例子是當你要往資料庫中輸入資料時。 例如,將名字 O’reilly 插入到資料庫中,這就需要對其進行轉義。 強烈建議使用 DBMS 指定的轉義函式 (比如 MySQL 是 mysqli_real_escape_string(),PostgreSQL 是 pg_escape_string()),但是如果你使用的 DBMS 沒有一個轉義函式,並且使用 \ 來轉義特殊字元,你可以使用這個函式。僅僅是為了獲取插入資料庫的資料,額外的 \ 並不會插入 。 當 PHP 指令 magic_quotes_sybase 被設定成 on 時,意味著插入 ‘ 時將使用 ‘ 進行轉義。
PHP 5.4 之前 PHP 指令 magic_quotes_gpc 預設是 on, 實際上所有的 GET、POST 和 COOKIE 資料都用被 addslashes() 了。不要對已經被 magic_quotes_gpc 轉義過的字串使用 addslashes(),因為這樣會導致雙層轉義。 遇到這種情況時可以使用函式 get_magic_quotes_gpc() 進行檢測。
程式碼示例:
<?php $str = "Is your name O'reilly?"; // 輸出: Is your name O\'reilly? echo addslashes($str); ?>
0X02 stripslashes() –>(PHP 4, PHP 5, PHP 7)
用法:
string stripslashes ( string $str )
反引用一個引用字串,如果 magic_quotes_sybase 項開啟,反斜線將被去除,但是兩個反斜線將會被替換成一個。
返回值:
返回一個去除轉義反斜線後的字串(\’ 轉換為 ‘ 等等)。雙反斜線(\)被轉換為單個反斜線(\)。
程式碼示例:
<?php function stripslashes_deep($value) { $value = is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value); return $value; } // 範例 $array = array("f\\'oo", "b\\'ar", array("fo\\'o", "b\\'ar")); $array = stripslashes_deep($array); // 輸出 print_r($array); ?>
結果:
Array ( [0] => f'oo [1] => b'ar [2] => Array ( [0] => fo'o [1] => b'ar ) )
0X03 addcslashes() –>(PHP 4, PHP 5, PHP 7)
用法:
string addcslashes ( string $str , string $charlist )
返回值:
返回字串,該字串在屬於引數 charlist 列表中的字元前都加上了反斜線。
示例程式碼:
這段程式碼就是告訴我們要注意選取的字元的範圍,大寫字母和小寫字母中間還有一些可見字元,另外起始字元的ascII 碼要小於結束符的,否則達不到預期的效果,只能是轉義這個幾個列出來的
<?php echo addcslashes('foo[ ]', 'A..z'); // 輸出:\f\o\o\[ \] // 所有大小寫字母均被轉義 // ... 但 [\]^_` 以及分隔符、換行符、回車符等也一併被轉義了。 ?>
注意:當選擇對字元 0,a,b,f,n,r,t 和 v 進行轉義時需要小心,它們將被轉換成 \0,\a,\b,\f,\n,\r,\t 和 \v。在 PHP 中,只有 \0(NULL),\r(回車符),\n(換行符)和
\t(製表符)是預定義的轉義序列, 而在 C 語言中,上述的所有轉換後的字元都是預定義的轉義序列。
0X04 stripcslashes() –>(PHP 4, PHP 5, PHP 7)
用法:
string stripcslashes ( string $str )
返回值:
返回反轉義後的字串。可識別類似 C 語言的 \n,\r,… 八進位制以及十六進位制的描述。
示例程式碼:
stripcslashes('He\xallo') == 'He'."\n".'llo' stripcslashes('H\xaello') == 'H'.chr(0xAE).'llo'
0X05 mysql_escape_string() –>(PHP 4 >= 4.0.3, PHP 5)
用法:
string mysql_escape_string ( string $unescaped_string )
mysql_escapestring() 並不轉義 % 和 。 本函式和 mysql_real_escape_string() 完全一樣,除了 mysql_real_escape_string() 接受的是一個連線控制代碼並根據當前字符集轉移字串之外。mysql_escape_string() 並不接受連線引數,也不管當前字符集設定。
示例程式碼:
<?php $item = "Zak's Laptop"; $escaped_item = mysql_escape_string($item); printf ("Escaped string: %s\n", $escaped_item); ?>
結果:
Escaped string: Zak\'s Laptop
0X06 mysql_real_escape_string() –>(PHP 4 >= 4.3.0, PHP 5)
用法:
string mysql_real_escape_string ( string $unescaped_string [, resource $link_identifier = NULL ] )
本函式將 unescaped_string 中的特殊字元轉義,並計及連線的當前字符集,因此可以安全用於 mysql_query()。
mysql_real_escape_string() 呼叫mysql庫的函式 mysql_real_escape_string, 在以下字元前新增反斜槓: \x00 \n \r \ ' " \x1a.
為了安全起見,在像MySQL傳送查詢前,必須呼叫這個函式(除了少數例外情況)。
注意:本擴充套件自 PHP 5.5.0 起已廢棄,並在自 PHP 7.0.0 開始被移除。應使用 MySQLi 或 PDO_MySQL 擴充套件來替換之。
0X07 PHP 魔術引號 –> (< PHP 5.4)
1.什麼是魔術引號
當開啟時,所有的 ‘(單引號),”(雙引號),\(反斜線)和 NULL 字元都會被自動加上一個反斜線進行轉義。這和 addslashes() 作用完全相同。
一共有三個魔術引號指令:
(1)magic_quotes_gpc影響到 HTTP 請求資料(GET,POST 和 COOKIE)。不能在執行時改變。在 PHP 中預設值為 on。
程式碼示例:
<?php // 如果啟用了魔術引號 echo $_POST['lastname'];// O\'reilly echo addslashes($_POST['lastname']); // O\\\'reilly // 適用各個 PHP 版本的用法 if (get_magic_quotes_gpc()) { $lastname = stripslashes($_POST['lastname']); } else { $lastname = $_POST['lastname']; } // 如果使用 MySQL $lastname = mysql_real_escape_string($lastname); echo $lastname; // O\'reilly $sql = "INSERT INTO lastnames (lastname) VALUES ('$lastname')"; ?>
(2)magic_quotes_runtime如果開啟的話,大部份從外部來源取得資料並返回的函式,包括從資料庫和文字檔案,所返回的資料都會被反斜線轉義。該選項可在執行的時改變,在 PHP 中的預設值為 off。
程式碼示例:
<?php // 建立臨時檔案指標 $fp = tmpfile(); // 寫入一些資料 fwrite($fp, '\'PHP\' is a Recursive acronym'); // 沒有 magic_quotes_runtime rewind($fp); set_magic_quotes_runtime(false); echo 'Without magic_quotes_runtime: ' . fread($fp, 64), PHP_EOL; // 有 magic_quotes_runtime rewind($fp); set_magic_quotes_runtime(true); echo 'With magic_quotes_runtime: ' . fread($fp, 64), PHP_EOL; // 清理 fclose($fp); ?>
magic_quotes_gpc與magic_quotes_runtime的區別
1.magic_quotes_runtime是對外部引入的資料庫資料或者檔案中的特殊字元進行轉義,而magic_quotes_gpc是對post、get、cookie等陣列傳遞過來的資料進行特殊字元轉義。
2.他們都有相應的get函式,可以對php環境中是否設定了他們相應功能特性進行探測,如:get_magic_quotes_gpc,是對magic_quotes_gpc是否設定的探測,get_magic_quotes_runtime,是對magic_quotes_runtime是否設定的探測,而且都是如果設定了,get函式返回1,如果沒有設定,get函式返回0。
3.不能在程式裡面設定magic_quotes_gpc的值,原因是php中並沒有set_magic_quotes_gpc這個函式,而magic_quotes_runtime有對應的能在程式碼中直接設定magic_quotes_runtime值的函式:set_magic_quotes_runtime,所以,magic_quotes_gpc的值,只能自己手動在php.ini檔案裡面設定了。
(3)magic_quotes_sybase
如果該選項在php.ini檔案中是唯一開啟的話,將只會轉義%00為\0(即null字元)。此選項會完全覆蓋magic_quotes_gpc。如果同時開啟這兩個選項的話,單引號將會被轉義成兩個單引號,%00會被轉義為\0。而雙引號、反斜線將不會進行轉義
1.設定:magic_quotes_sybase = On & magic_quotes_gpc = Off
輸入:
1'2"3\4%005
結果:
1'2"3\45
結論:
只將%00(即null字元)過濾了
2.設定:magic_quotes_sybase = On & magic_quotes_gpc = On
輸入:
1'2”3\4%005
結果:
1''2"3\4\05
結論:
magic_quotes_sybase = On & magic_quotes_gpc = On時,magic_quotes_sybase將會使用單引號對單引號進行轉義,%00(即null字元)也會被轉義。
2.為什麼存在魔術引號
沒有理由再使用魔術引號,因為它不再是 PHP 支援的一部分。不過它幫助了新手在不知不覺中寫出了更好(更安全)的程式碼。但是在處理程式碼的時候,最好是更改你的程式碼而不是依賴於魔術引號的開啟。 為什麼這個功能存在?是為了阻止SQL 注入。在今天,開發者能夠更好得意識到了安全問題,並最終使用資料庫轉移機制或者 prepared語句來取代魔術引號功能。
3.為什麼不用魔術引號
(1)可移植性
程式設計時認為其開啟或並閉都會影響到移植性。可以用 get_magic_quotes_gpc() 來檢查是否開啟,並據此程式設計。
(2)效能
由於並不是每一段被轉義的資料都要插入資料庫的,如果所有進入 PHP 的資料都被轉義的話,那麼會對程式的執行效率產生一定的影響。在執行時呼叫轉義函式(如 addslashes())更有效率。 儘管 php.ini-dist 預設打開了這個選項,但是 php.ini-recommended 預設卻關閉了它,主要是出於效能的考慮。
(3)不便
由於不是所有資料都需要轉義,在不需要轉義的地方看到轉義的資料就很煩。比如說通過表單傳送郵件,結果看到一大堆的 \’。針對這個問題,可以使用 stripslashes() 函式處理。
0X08 mysqli_real_escape_string/mysqli_escape_string–> (PHP >= 5 ,PHP 7)
此函式用來對字串中的特殊字元進行轉義, 以使得這個字串是一個合法的 SQL 語句。傳入的字串會根據當前連線的字符集進行轉義,得到一個編碼後的合法的 SQL 語句。mysqli_escape_string 是 mysqli_real_escape_string 的別名。
用法:
mysqli_real_escape_string(connection,escapestring);
引數解釋:
connection必需。規定要使用的 MySQL 連線。
escapestring必需。要轉義的字串。編碼的字元是 NUL(ASCII 0)、\n、\r、\、’、” 和 Control-Z。
返回值:
返回已轉義的字串。
注意:
1.呼叫 mysqli_real_escape_string() 函式之前, 必須先通過呼叫 mysqli_set_charset() 函式或者在 MySQL 伺服器端設定字符集
2.mysqli_character_set_name() 返回當前資料庫連線的預設字元編碼
0X09prepare 預編譯
通過使用預編譯語句 (prepared statements)和引數化查詢 (parameterized queries)。這些sql語句從引數,分開的傳送到資料庫服務端,進行解析。這樣黑客不可能插入惡意sql程式碼。
對應的就是下面這兩種方法:
1.使用PDO物件(對於任何資料庫驅動都好用)
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute(array('name' => $name)); foreach ($stmt as $row) { // do something with $row }
2. 使用MySqli
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?'); $stmt->bind_param('s', $name); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { // do something with $row }
正確地建立連線:
注意:當使用PDO去連線Mysql資料庫時,真正的預處理預設並沒有開啟。為了開啟他,你應該關閉模擬的預處理語句,以下是一個例子:
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass'); $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
在上面的例子裡,錯誤模式嚴格意義上來說沒有必要,但推薦你加上去。這樣,指令碼在遇到致命錯誤(Fatal Error)的時候並不會停止執行。並且給開發者去捕獲(catch )那些PDOException異常。
第一個setAttribute()是必須的。這告訴PDO去關閉模擬預處理,然後使用真正的預處理語句。這將保證語句和值在被交到Mysql伺服器上沒有被解析(讓攻擊者沒有機會去進行sql注入。)
儘管你可以在建構函式裡設定字符集(charset ),但你也要注意舊版本的PHP(<5.3.6)會忽略在DSN中設定的字符集引數。
解釋
到底發生了什麼呢?你的SQL語句交給prepare 之後被資料庫伺服器解析和編譯了 。通過制定引數(不管是“?”還是命名佔位符:name),你都可以告訴資料庫引擎哪裡你想過濾掉。然後當你執行execute方法時,預處理語句會把你所指定的引數值結合起來。
這裡很重要的就是引數值和編譯過的語句繫結在了一起,而不是簡簡單單的SQL字串、SQL注入通過騙起指令碼加入一些惡意的字串,在建立sql傳送到資料庫的時候產生後果。所以,通過分離的從引數中傳送真正的sql語句,你控制了風險 :在結尾的時候你不打算乾的一些事。(譯者注:請看開篇的例子)。當你使用預編譯的時候,任何引數都會被當作字串。在這個例子裡,如果$name變數包含了’Sarah’; DELETE FROM employees 這個結果只會簡單的搜尋字串“‘Sarah’; DELETE FROM employees”,所以你不會得到一張空表。
另外一個使用預編譯的好處就是,如果你在同一個會話中執行一個statement多次,只會被解析和編譯一次,對速度更友好。
哦,既然你問了增加語句的時候怎麼使用,下面給你個例子:
$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)'); $preparedStatement->execute(array('column' => $unsafeValue));
PDO如何解決sql注入
完整程式碼:
<?php $pdo = new PDO("mysql:host=192.168.0.1;dbname=test;charset=utf8","root"); $st = $pdo->prepare("select * from info where id =? and name = ?"); $id = 21; $name = 'zhangsan'; $st->bindParam(1,$id); $st->bindParam(2,$name); $st->execute(); $st->fetchAll(); ?>
在php5.3.6之後,pdo不會在本地對sql進行拼接然後將拼接後的sql傳遞給mysql server處理(也就是不會在本地做轉義處理)。pdo的處理方法是在prepare函式呼叫時,將預處理好的sql模板(包含佔位符)通過mysql協議傳遞給mysql server,告訴mysql server模板的結構以及語義。 當呼叫execute時,將兩個引數傳遞給mysql server。由mysql server完成變數的轉移處理。將sql模板和變數分兩次傳遞,即解決了sql注入問題。
0X10 補充:使用了PDO就一定安全了嗎???
建議去看一下PDO 的官方文件,文章中有這樣一句話:
(however,
if other portions of the query are being built up with unescaped
input, SQL injection is still possible).
翻譯過來就是
開發人員可以確保不會發生SQL注入(然而,如果查詢的其他部分是用未轉義輸入構建的,那麼SQL注入就仍然可能)。
因為有些查詢語句並不適合使用PDO 進行處理,可能使用PDO處理比較困困難,於是就有一些沒有做處理,還有就是有些掛羊頭賣狗肉(估計開發的也不懂PDO),真正用的時候還是老方法,再有就是開發人員對PDO本地預處理的錯誤開放,以及一些編碼問題的處理上可能還是存在問題。
當然這是面試經常問的問題,請看這三篇文章,雖然有點老,但是我認為對原理的理解還是很有幫助的。