1.使用一個SQL注射備忘單
一個基本的原則就是,永遠不要相信使用者送出的資料。
另一個規則就是,在你發送或者存儲資料時對它進行轉義(escape)。
可以總結為:filter input, escape output (FIEO). 輸入過濾,輸出轉義。
通常導緻SQL注射漏洞的原因是沒有對輸入進行過濾,如下語句:
1 2 3 4 | <?php $query = "SELECT * FROM users WHERE name = '{$_GET['name']}'"; |
在這個例子中,$_GET['name']來自使用者送出的資料,既沒有進行轉義,也沒有進行過濾~~
對于轉義輸出,你要記住用于你程式外部的資料需要被轉義,否則,它可能被錯誤地解析。
相反,過濾輸入能確定資料在使用前是正确的.
對于過濾輸入,你要記住,在你程式外部的原始資料需要被過濾,因為它們是不可信任的。
如下例子示範了輸入過濾和輸出轉義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php // Initialize arrays for filtered and escaped data, respectively. $clean = array(); $sql = array(); // Filter the name. (For simplicity, we require alphabetic names.) if (ctype_alpha($_GET['name'])) { $clean['name'] = $_GET['name']; } else { // The name is invalid. Do something here. } // Escape the name. $sql['name'] = mysql_real_escape_string($clean['name']); // Construct the query. $query = "SELECT * FROM users WHERE name = '{$sql['name']}'"; ?> |
另一個有效防止SQL注射的方法是使用prepare 語句,如:
1 2 3 4 5 6 7 8 9 10 11 | <?php // Provide the query format. $query = $db->prepare('SELECT * FROM users WHERE name = :name'); // Provide the query data and execute the query. $query->execute(array('name' => $clean['name'])); ?> |
2.了解比較運算符之間的不同
例如,你使用strpos() 來檢測在一個字元串中是否存在一個子串 (如果子串沒有找到,函數将傳回 FALSE ), 結果可能會導緻錯誤:
1 2 3 4 5 6 7 8 9 | <?php $authors = 'Chris & Sean'; if (strpos($authors, 'Chris')) { echo 'Chris is an author.'; } else { echo 'Chris is not an author.'; } ?> |
上例中,由于子串處于最開始的位置,是以strpos() 函數正确地傳回了0,表明子串處于字元串中最開始的位置。然後,因為條件語句會把結果當成Boolean(布爾)類型的,是以 0 就被PHP給計算成了 FALSE,最終導緻條件語句判斷失敗。
當然,這個BUG可以用嚴格的比較語句來修正:
1 2 3 4 5 6 7 8 9 | <?php if (strpos($authors, 'Chris') !== FALSE) { echo 'Chris is an author.'; } else { echo 'Chris is not an author.'; } ?> |
3.減少else(Shortcut the else)
記住,在你使用變量前總是要先初始化它們。
考慮如下一個用來根據使用者名來檢測使用者是否是管理者的條件語句:
1 2 3 4 5 6 7 8 9 | <?php if (auth($username) == 'admin') { $admin = TRUE; } else { $admin = FALSE; } ?> |
這個看起來似乎足夠安全,因為看一眼就很容易了解。想象一下有一個更複雜一點的例子,它為name和email同時設定變量,為友善起見:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php if (auth($username) == 'admin') { $name = 'Administrator'; $email = '[email protected]'; $admin = TRUE; } else { /* Get the name and email from the database. */ $query = $db->prepare('SELECT name, email FROM users WHERE username = :username'); $query->execute(array('username' => $clean['username'])); $result = $query->fetch(PDO::FETCH_ASSOC); $name = $result['name']; $email = $result['email']; $admin = FALSE; } ?> |
因為 $admin 還是明确地被設定為TRUE or FALSE,似乎一切都完好。但是,如果另一個開發者後來在代碼裡加了一個elseif語句,很可能他會忘記這回事:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?php if (auth($username) == 'admin') { $name = 'Administrator'; $email = '[email protected]'; $admin = TRUE; } elseif (auth($username) == 'mod') { $name = 'Moderator'; $email = '[email protected]'; $moderator = TRUE; } else { /* Get the name and email. */ $query = $db->prepare('SELECT name, email FROM users WHERE username = :username'); $query->execute(array('username' => $clean['username'])); $result = $query->fetch(PDO::FETCH_ASSOC); $name = $result['name']; $email = $result['email']; $admin = FALSE; $moderator = FALSE; } ?> |
如果一個使用者提供一個能夠觸發elseif條件的使用者名(username), $admin 沒有被初始化,這可能會導緻不必要的行為,或者更糟糕的情況,一個安全漏洞。另外,一個類似的情況對于 $moderator 變量來說同樣存在,它在第一個條件中沒有被初始化。
通過初始化$admin 和 $moderator ,這是完全很容易避免這一情況的發生的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <?php $admin = FALSE; $moderator = FALSE; if (auth($username) == 'admin') { $name = 'Administrator'; $email = '[email protected]'; $admin = TRUE; } elseif (auth($username) == 'mod') { $name = 'Moderator'; $email = '[email protected]'; $moderator = TRUE; } else { /* Get the name and email. */ $query = $db->prepare('SELECT name, email FROM users WHERE username = :username'); $query->execute(array('username' => $clean['username'])); $result = $query->fetch(PDO::FETCH_ASSOC); $name = $result['name']; $email = $result['email']; } ?> |
不管剩下的代碼是什麼,現在已經明确了 $admin 值 為FALSE ,除非它被顯式地設定為其它值。對于 $moderator 也是一樣的。最壞的可能發生的情況就是,在任何條件下都沒有修改$admin 或 $moderator ,導緻某個是administrator 或moderator的人沒有被當作相應的administrator 或moderator 。
如果你想 shortcut something ,并且你看到我們的例子有包含有else覺得有點失望。我們有一個bonus tip 你可能會感興趣的。我們并不确定它可以被認為是a shortcut,但是我們希望它仍然是有幫助的。
考慮一下一個用于檢測一個使用者是否被授權檢視一個特定頁面的函數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php function authorized($username, $page) { if (!isBlacklisted($username)) { if (isAdmin($username)) { return TRUE; } elseif (isAllowed($username, $page)) { return TRUE; } else { return FALSE; } } else { return FALSE; } } ?> |
這個例子是相當的簡單,因為隻有三條規則需要考慮:
administrators 總是被允許通路的,
處于黑名單的永遠是禁止通路的,
isAllowed()決定其它人是否有權通路。
(還有一個特例是:當一個administrator 處于黑名單中,但這似乎是不太可能的事,是以我們這裡直接忽視這種情況)。
我們使用函數來做這個判斷以保持代碼的簡潔然後集中注意力到業務邏輯上去。
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php function authorized($username, $page) { if (!isBlacklisted($username)) { if (isAdmin($username) || isAllowed($username, $page)) { return TRUE; } else { return FALSE; } } else { return FALSE; } } ?> |
事實上,你可以精減整個函數到一個複合條件:
1 2 3 4 5 6 7 8 9 10 11 | <?php function authorized($username, $page) { if (!isBlacklisted($username) && (isAdmin($username) || isAllowed($username, $page)) { return TRUE; } else { return FALSE; } } ?> |
最後,這個可以被減少到隻有一個return:
1 2 3 4 5 6 7 | <?php function authorized($username, $page) { return (!isBlacklisted($username) && (isAdmin($username) || isAllowed($username, $page)); } ?> |
如果你的目标是謄清代碼的行數,那麼這樣你做到的。但是,你要注意到,我們在用isBlacklisted(), isAdmin() 和 isAllowed() ,這取決于參與這些判斷的東西,減少代碼到隻剩下一個複合條件可能不吸引人。
這下說到我們的小技巧上了,一個“立即傳回”函數,是以,如果你盡快傳回,你可以很簡單地表達這些規則:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?php function authorized($username, $page) { if (isBlacklisted($username)) { return FALSE; } if (isAdmin($username)) { return TRUE; } return isAllowed($username, $page); } ?> |
這個例子使用了更多行數的代碼,但是它是非常簡單和不惹人注意的。更重要的是,這個方法減少了你必需考慮的上下文的數量。例如,一旦你決定了使用者是否處于黑名單裡面,你就可以安全地忘掉這件事了。特别是你的邏輯很複雜的時候,這是相當的有幫助的。
4. 總是使用大括号
PS:原諒是“扔掉那些方括号 Drop Those Brackets”
根據本文的内容, 我們相應作者的意思應該是 “braces,” 而不是brackets. “Curly brackets” 可能有大括号的意思, 但是”brackets” 通常表示 “方括号”的意思。這個技巧應該被無條件的忽略,因為,沒有大括号,可讀性和可維護性被破壞了。
舉一個簡單的例子:
1 2 3 4 5 6 7 8 9 | <?php if (date('d M') == '21 May') $birthdays = array('Al Franken', 'Chris Shiflett', 'Chris Wallace', 'Lawrence Tureaud'); ?> |
If you’re good enough, smart enough, secure enough, notorious enough, or pitied enough, 你可能會想在5月21号參加社交聚會:
1 2 3 4 5 6 7 8 9 10 | <?php if (date('d M') == '21 May') $birthdays = array('Al Franken', 'Chris Shiflett', 'Chris Wallace', 'Lawrence Tureaud'); party(TRUE); ?> |
沒有大括号,這個簡單的條件導緻你每天參加社交聚會 。也許你有毅力,是以這個錯誤是一個受歡迎的。希望那個愚蠢的例子并不分散這一的觀點,那就是過度狂歡是一種出人意料的副作用。
為了提倡丢掉大括号,先前的文章使用類似下面的簡短的語句作為例子:
1 2 3 4 | <?php if ($gollum == 'halfling') $height --; else $height ++; ?> |
因為每個條件被放在單獨的一行, 這種錯誤似乎會較少發生, 但是這将導緻另一個問題:代碼的不一緻和需要更多的時間來閱讀和了解。一緻性是這樣一個重要的特性,以緻開發人員經常遵守一個編碼标準,即使他們不喜歡編碼标準本身。
我們提倡總是使用大括号:
1 2 3 4 5 6 7 8 9 10 11 | <?php if (date('d M') == '21 May') { $birthdays = array('Al Franken', 'Chris Shiflett', 'Chris Wallace', 'Lawrence Tureaud'); party(TRUE); } ?> |
你天天聚會是沒關系的,但要保證這是經過思考的,還有,請一定要邀請我們!
5. 盡量用str_replace() 而不是 ereg_replace() 和 preg_replace()
我們讨厭聽到的否認的話,但是(原文)這個用于示範誤用的小技巧導緻了它試圖避免的同樣的濫用問題。(
We hate to sound disparaging, but this tip demonstrates the sort of misunderstanding that leads to the same misuse it’s trying to prevent.)
很明顯字元串函數比正規表達式函數在字元比對方面更快速高效,但是作者糟糕地試圖從失敗中得出一個推論:
(FIX ME: It’s an obvious truth that string functions are faster at string matching than regular expression functions, but the author’s attempt to draw a corollary from this fails miserably:)
If you’re using regular expressions, then ereg_replace() and preg_replace() will be much faster than str_replace().
Because str_replace() does not support pattern matching, this statement makes no sense. The choice between string functions and regular expression functions comes down to which is fit for purpose, not which is faster. If you need to match a pattern, use a regular expression function. If you need to match a string, use a string function.
6. 使用三重運算符
三元運算符的好處是值得讨論的. 下面是一行從最近我們進行的審計的代碼中取出的:
1 2 3 4 5 | <?php $host = strlen($host) > 0 ? $host : htmlentities($host); ?> |
啊,作者的真實意願是如果字元串的長度大于0 就轉義 $host , 但是卻意外地做了相反的事情。很容易犯的錯誤是吧?也許吧。在代碼審計過程中很容易錯過?當然。簡潔并不一定能使代碼變得很好。
三重運算符對于單行,原型,和模闆也行是适合的,但是我們相信一個普通的條件語句總是更好的。PHP是描述性的和詳細的,我們認為代碼也應該是。
7. Memcached
磁盤通路是慢速的,網絡通路也是慢的,資料庫通常使用這二者。
記憶體是很快的。使用本地緩存可以避免網絡和磁盤通路的開銷。結合這些道理,然後,你想到了memcached,一個“分布式記憶體對象緩存系統”,最初為基于Perl的部落格平台LiveJournal開發的。
如果你的程式不是分布在多個伺服器上,你可能并不需要memcached。單的緩存方法——序列化資料然後将它儲存在一個臨時檔案中。例如 – 對每個請求可以消除很多多餘的工作。事實上,這是我們考慮幫助我們的客戶優化他們的應用程式時,低挂水果的類型。
什麼是low-hanging fruit:
A fruit-bearing tree often contains some branches low enough for animals and humans to reach without much effort. The fruit contained on these lower branches may be not be as ripe or attractive as the fruit on higher limbs, but it is usually more abundant and easier to harvest. From this we get the popular expression “low hanging fruit”, which generally means selecting the easiest targets with the least amount of effort.
一種最簡易且最通用的将資料緩存在記憶體的方式是使用APC中的共享類型輔助方法,APC是一個最初由我們的同僚George Schlossnagle開發的緩存系統,考慮如下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php $feed = apc_fetch('news'); if ($feed === FALSE) { $feed = file_get_contents('http://example.org/news.xml'); // Store this data in shared memory for five minutes. apc_store('news', $feed, 300); } // Do something with $feed. ?> |
使用這種類型的緩存,你不必在每一次請求時等待遠端伺服器發送Feed資料。一些延遲産生了 – 在這個例子中上限是五分鐘,但可以根據您的應用程式需要調整到接近實時。
8. 使用架構
所有決定都會有結果的,我們喜歡架構——事實上,CakePHP 和 Solar 的主要開發者和我們一起在 OmniTI 工作—— 但是使用一個架構并不會奇迹般地使你在做的東西變得更好。
在十月份,我們的同僚Paul Jones為HP Advent寫一了篇文章,叫做The Framework as Franchise ,在文章中他将架構與商業專營權相比較。他引用 Michael Gerber “電子神話再現”(”The E-Myth Revisited”) 一書中的建議:
格柏指出,運作一個成功的企業,企業家需要像他将要賣掉他的企業作為一個特許經營權的原型一樣行動。這是企業擁有者可以不親自參與每一項決策使企業營運的唯一方法。
( Gerber notes that to run a successful business, the entrepreneur needs to act as if he is going to sell his business as a franchise prototype. It is the only way the business owner can make the business operate without him being personally involved in every decision.)
這是一個好的建議。無論你是打算使用架構或者定義你自己的标簽和慣例,從未來開發者的角度來看價值是很重要的。
雖然我們很樂意給你一個放之四海而皆準的真理,延伸這個想法來表明一個架構總是合适的,并不是我們想做的事情。
如果你問我們是否應該使用一個架構,我們可以給出的最好的答案是,“這要看情況。”
9. 正确的使用錯誤抑制操作符
總是試着避免使用錯誤抑制操作符号。在前面的文章,作者表明:
@ 操作符是相當的慢的并且如果你需要寫高性能的代碼的話它會使得開銷很大。
錯誤抑制慢是因為在執行抑制語句前,PHP動态的改變error_reporting等級到0 ,然後然後立即将其還原。這是要開銷的。
更糟糕的是,使用錯誤抑制符使追蹤問題的根本原因很困難。
先前的文章使用如下例子來支援通過引用來給一個變量指派的做法。。。(這句怎麼翻譯?我暈~~~ )
The previous article uses the following example to support the practice of assigning a variable by reference when it is unknown if $albus is set:
1 2 3 4 5 | <?php $albert =& $albus; ?> |
盡管這樣是工作的——對于現在——依靠奇怪的,未定義的行為,而對于為什麼這樣會工作有一個很好的了解是一個産生BUG的好方法。
因為 $albert 是引用了$albus的,後期對于$albus的修改将會同樣影響到$albert .
一個更好的解決方案是使用isset(),加上大括号:
1 2 3 4 5 6 7 | <?php if (!isset($albus)) { $albert = NULL; } ?> |
給$albert 指派NULL和給它賦一個不存在的引用的效果是相同的,但是更加明确了,大大提高了代碼的清晰度和避免的兩個變量之間的引用關系。
If you inherit code that uses the error suppression operator excessively, we’ve got a bonus tip for you. There is a new PECL extension called Scream that disables error suppression.
10. 使用 isset() 而不是 strlen()
這實際上是一個巧妙的方法,雖然前面的文章完全沒有解釋這個。下面是補充的例子:
1 2 3 4 5 6 7 | <?php if (isset($username[5])) { // The username is at least six characters long. } ?> |
當你把字元串當作一個數組時(荒野無燈:事實上,在C語言裡面,字元中通常以數組形式存在),字元串裡的每一個字元都是數組的一個元素。通過檢測一個特定元素的存在與否,你可以檢測這個字元串是否至少有那麼多的字元存在。(注意第一個字元是元素0,是以 $username[5] 是 $username中的第6個字元。)
這樣使用isset 比strlen稍快的原因是複雜的。簡單的解釋是,strlen() 是一個函數,而 isset() 是一個文法結構。通常來說,
調用一個函數是比使用語言結構的代價更為昂貴的。