什么叫天坑。天吶,原來這么坑,不知則已,細思極恐。
一、小數(符點數)不能直接比較是否相等
比如 if( 8-6.4==1.6 ) 的結果是 false。究其原因是因為,PHP是基於C語言的,而C語言由於其二進制符點數的表示方式,導致不能精確表示大多數符點數。實際上,幾乎所有的編程語言都沒能精確表示小數(符點數),這是一個普遍存在的現象,因為這個是 IEEE 754 的缺陷。想要解決此問題,只能另立標准,似乎只有Mathematica解決了此問題。
PHP 中可以使用BC數學函數系列中的 bccomp()函數 比較小數是否相等。
二、字符串是否相同建議用 === 而非 ==
為什么呢?因為這個比較是弱類型,兩個比較時,PHP會先嘗試判別左右兩者是否為數字。而問題就在於什么樣的字符串是數字,是單純的數字串嗎?遠遠不只於此,還包括 0x 開頭的十六進制,XXeX類型的科學記數法 等等,如 '12e0'=='0x0C' 得到的是true。而在數值類型與字符串比較時,甚至一些數字開頭的非數值串,比如 12=='12這個串' 得到的值也會是 true。
所以這些情況下,可能會使本來並不相同的字符串被判定為相等。而使用===比較則為包含類型的比較,不會有任何轉換,所以是可以准確比較字符串是否相同的。
另外吐槽一下JAVA,==居然比較不了字符串是否相等,因為字符串是一個對象,==變成了判斷是否為同一個對象……
三、trim系列函數的過多去除
trim函數的基本用法是去除最外邊的空格、換行符之類的。因為其可選參數,很多人也會將其用於去除UTF8BOM頭、文件擴展名等等,比如 ltrim($str, "\xEF\xBB\xBF"); rtrim($str, ".txt"); 。但是很快,就會發現這些函數會多去除了一些東西,比如本來是想去除后綴的,結果 logtext.txt 會變成了 logte 而不是 logtext。為什么呢?因為后面這個參數的意思不是一個完整字符串,而是字符列表,也就是說會一直檢查最左/最右是否符合此列表的其中一個。
那怎么樣才是真正我們想要的去掉最前最后呢?網上的說法是說用正則表達式,我封裝了對應的三個方法,以便使用。命名規則是比原來PHP的函數多了個s,表示string的意思。用法跟原來PHP的函數一樣。
/** * 另一種 trim ,不會過多去除 * $charlist正則元字符會自動轉義 * */ function ltrims($str, $charlist){ $charlist = preg_quote($charlist, '/'); return preg_replace("/^$charlist/", '', $str); } function rtrims($str, $charlist){ $charlist = preg_quote($charlist, '/'); return preg_replace("/{$charlist}$/", '', $str); } function trims($str, $charlist){ $charlist = preg_quote($charlist, '/'); $str = preg_replace("/^{$charlist}/", '', $str);; return preg_replace("/{$charlist}$/", '', $str); } function trimBOM($str){ return preg_replace("/^\xEF\xBB\xBF/", '', $str); }
四、網上說的獲取客戶端IP地址的各種方法
網上流行一段獲取客戶端IP地址的PHP函數如下:
function getIP() { if (getenv('HTTP_CLIENT_IP')) { $ip = getenv('HTTP_CLIENT_IP'); }elseif (getenv('HTTP_X_FORWARDED_FOR')) { $ip = getenv('HTTP_X_FORWARDED_FOR'); }elseif (getenv('HTTP_X_FORWARDED')) { $ip = getenv('HTTP_X_FORWARDED'); }elseif (getenv('HTTP_FORWARDED_FOR')) { $ip = getenv('HTTP_FORWARDED_FOR');} }elseif (getenv('HTTP_FORWARDED')) { $ip = getenv('HTTP_FORWARDED'); }else { $ip = $_SERVER['REMOTE_ADDR']; } return $ip; }
這函數看起來並沒有什么問題,很多開源CMS之類的也在用。然而事實上,問題大着呢!首先第一步,是要了解這些 getenv 讀取的東西到底是什么玩意,又是從哪來的。簡單來說這些其實是HTTP header,有些代理服務器會把源請求地址放到header里,所以我們服務器可以知道訪問用戶的原始IP地址。但是,並不是所有代理服務器都會這么做,也並不是只有代理服務器會這么做。
而實際上,這些HTTP header是可以隨便改動的,比如curl就可以自己設置各種HTTP header。如果用此函數得到的結果,進行IP限制等操作的話是很輕易繞過的。更可怕的是,如果后續程序沒有對此函數取得的IP地址進行格式校驗過濾的話,就很微妙地為SQL注入打開了一扇窗戶。所以比較保險的方式是只讀取非HTTP header的 $_SERVER['REMOTE_ADDR']
PHP5.4及以上可以使用以下函數判斷是否符合IP地址格式 filter_var($ip, FILTER_VALIDATE_IP) ,老版本需自行寫正則。
五、foreach的保留現象
使用 foreach($someArr as $someL){ } 之類的用法時,要注意最后的一個 $someL 會一直保留到該函數/方法結束。而當使用引用的時候 foreach($someArr as &$someL){ }這是以引用來保存,也就是說后面若有使用同一個名字的變量名,將會把原數據改變(就像一個亂用的C指針)。為安全起見,建議每個foreach(尤其是引用的)結束之后都使用unset把這些變量清除掉。
foreach($someArr as &$someL){ //doSomething ... }unset($someL);
六、htmlspecialchars 函數默認不轉義單引號
不少網站都是使用此函數作為通用的輸入過濾函數,但是此函數默認情況是不過濾單引號的。這是非常非常地容易造成XSS漏洞。這樣的做法和不過濾雙引號沒太大區別,只要前端寫得稍微有點不規范(用了單引號)就會中招。下面這個示例改編自知乎梧桐雨的回答
<!Doctype html>
<meta charset="utf-8"/>
<?php
$name = $_POST["xxs"];
$name = htmlspecialchars($name); ?> <form action="" method="post"> 提交的注入<input type="text" name="xxs" value="ins' onclick='alert(1)" > <input type="submit" value="提交" /> </form> 提交后這個按鈕會被注入點擊彈警告 <input type='button' value='<?=$name?>' />
要求所有的時候都使用雙引號不得使用單引號,這其實不太現實。所以,這個主要還是后端的責任,把單引號也要轉義,我們用的時候一定要給這個函數加上參數 htmlspecialchars( $data, ENT_QUOTES);
很多人向Thinkphp框架提出過這個問題,因為其默認過濾方法就是無參數的htmlspecialchars,不過濾單引號,而其官方答復是“I函數的作用不能等同於防止SQL注入,可以自定義函數來過濾”……毛線啊,最基本的防護都不給力,這是給埋了多少隱患啊。在此強烈各位使用者重新定義默認過濾函數,我自己定義的是 htmlspecialchars(trim($data), ENT_QUOTES); ,有更好建議歡迎評論。同時非常希望TP官方更正此問題。
關於XSS,容我多說兩句,請看下面這個例子。
<?php $name='alert(1)'; ?> <p id="XSS2"></p> <script src="//cdn.batsing.com/jquery.js"></script> <script> $("#XSS2")[0].innerHTML = <?=$name?>;
$("#XSS2").html( <?=$name?> ); $("#XSS2")[0].innerHTML = "<?=$name?>";
$("#XSS2").html(" <?=$name?> "); </script>
其中第1、2行 JS會造成 XSS 漏洞,第3、4行則不會。而 alert(1) 這樣一種字符串,后端甚至沒有什么比較好的方法可以過濾,唯一有效的方法可能是在數據的兩端加上引號。主要責任還是在於前端,對 innerHTML 和 jQuery的html() 的輸出使用時,一定要確保傳入的參數是字符串,否則其危險性不亞於 eval 函數