构建安全的PHP应用(技术安全资讯)
譯者序
2015年7月的一天,我正在Feedly上悠然的閱讀,突然出現一篇博客有介紹這本書,因為說的是我當時正在關注的PHP安全,所以認真看了那篇博客,順著博客里的鏈接找到了這本書的官網,看起來很靠譜,而恰好我也需要,網站上有購買鏈接,一直到leanpub上我都在尋找是否有中文版可以購買,但沒有找到,而后在Twitter上聯系了作者Ben,問他關于中文版的事,遺憾的是還沒人翻譯過,當時就想這本書也不長,為什么我不來翻譯一下,為中國的開發者做點貢獻呢,當然也會收獲小小的成就感,隨后我們用郵件溝通了翻譯的詳細事項,只用了來回兩封郵件,不過幾百個單詞,交流真簡單明了,我愛上了這種高效簡單的溝通方式!當然以后如果還有這樣的機會,我想我還會繼續行好事,做想做該做的,不問前程!如果有翻譯不好的地方,可以隨時郵件我,我會非常積極的修改,這本書在相當長的一段時間都會以電子版的形式存在,所以當我修改了你提出的問題后,一定會最快的同步線上,這樣購買的所有人都會收到更新了,一起行好事吧!
寫在前面
幾年前我用PHP的CodeIgniter框架寫了一個網頁程序,但是這個框架并沒有內置任何類型的身份驗證系統。當然,這并不會難倒像我這樣的一個好(懶惰)的開發者,我到處尋找一個靠譜的庫來讓我的應用擁有健壯的身份驗證能力。然而令人失望的是我發現在CodeIgniter上并沒有一個簡潔、可靠并能滿足身份驗證需求的庫。這讓我走向了開發Ion Auth(可以從Github找到)之路,它是為CodeIgniter開發的一款輕量級的身份驗證庫,并在為網頁應用的安全上做了一個長時間的改革迭代,同時也幫助其他開發人員這樣做。
多年之后,我們都已經換了很多的框架和語言,但是我仍然對被忽視的基礎安全方面保持持續關注。讓我們一起改變這個現狀吧。我希望能夠幫助大家再也不用生活在密碼泄露的恐懼中,再不會為惡心的SQL注入而擔心,能夠輕松的避免那些“黑客”的臨幸。讓我們都能確保可以每天按時下班回家,并能高枕無憂的做閉目佳人!
這本書將會是一本可以在具體項目中進行參考的快速閱讀手冊。意思是你可以在數個小時內快速看完并在你需要的時候隨時查閱。我也會盡量使閱讀本書變得更加有趣。
格式說明
若無特殊說明,在本書中縮進內的示例代碼均為PHP。
以$符號開頭的行
$ ls -al
登錄后復制
是普通用戶下在命令行下的命令示例
以#符號開頭的行
# ls -al
登錄后復制
是root用戶命令行下的命令示例
服務器命令行示例會呈現*nix(centos, redhat, ubuntu, osx, etc)操作系統風格的樣式
我會努力讓示例代碼有合適的換行,這樣方法參數會在不同的行。這本書的代碼風格雖然看起來比較奇怪但是比那些封裝的代碼更容易閱讀。
勘誤表
如果您發現任何問題都可以毫不猶豫的聯系我的郵箱。
如果您有翻譯問題也可以聯系譯者郵箱
示例代碼
若無特殊說明,例子中的代碼均為PHP。我會盡可能使用原生的PHP,除非它造成了太多的冗余。當使用原生PHP來闡述問題太啰嗦時,我也會使用Laravel框架更優雅的風格讓大家更容易理解。
一些代碼過時或者不清楚,你可以在Github repository找到更全的代碼。
來吧!
關于作者
Ben Edmunds 帶領開發團隊做最前沿的網頁和手機應用。他是一個活躍積極的領導者、開發者,也是在多個開發社區的演講者。他已經在軟件研發領域專業研究十余載并做過從機器人技術到政府項目的各種工作。
PHP Town Hall 播客的聯合主持。波特蘭PHP Usergroup聯合組織者。開源項目倡導者。
關于譯者
張慶龍 技術上致力于web開發,主要使用PHP和Ruby on Rails,熱衷使用Mac進行開發和日常工作。熱愛生活,喜釣魚,愛表演,好搞小幽默。主要活動在京津冀地區。
第一章 - 不相信任何用戶,格式化所有的輸入!
我們從一個故事說起,麥克是俄克拉何馬州一個私立學校的系統管理員。他的主要工作是保持學校的網絡暢通和服務器穩定。最近他開始為學校開發一個可以自動完成各種任務的網頁應用以供內部使用。他沒有參加過正規的培訓而且是剛從一年前開始編程,但是他對自己的工作自我感覺良好。他了解一些PHP基礎而且已經為學校開發了一個有夠穩定的客戶關系管理系統。還有大量的功能等著添加,但是基本功能已經完備。麥克甚至由于精簡業務和節省學校開支獲得了來自院長的稱贊。
一切都很好,但是有一天一個特別的學生帶來了噩夢。這個學生的名字是Little Bobby Tables。這天,大喬在管理員辦公室打電話給麥克問為什么系統崩潰了。經過排查,麥克發現那個存有所有學生信息的數據庫表完全消失了。好家伙,Little Bobby的全名竟然是Robert'); DROP TABLE students;--,太尷尬了。這時候數據庫還沒有備份,這是麥克即將要做的一件事,但是還沒開始。這回麥克攤上大事兒了。
SQL 注入
真實的故事
雖然在真實的世界里名字中包括危險SQL語句的可能非常小,但是像這種SQL注入漏洞卻每天都在發生:
-
2012年,LinkedIn由于一個隱藏的SQL注入漏洞泄露了600萬用戶數據
-
2012年,雅虎!泄露了450,000的用戶密碼
-
2012年,Nvidia的400,000個密碼被盜
-
2012年,Adobe的150,000個密碼被盜
-
2013年,eHarmony泄露了大概150萬的用戶密碼
SQL注入的原理
如果你可以不加修飾的接受用戶直接輸入的內容,那么一個歹毒的用戶就可以傳入一個奇葩的數據,直接改變你的SQL語句。
假設你的代碼像下面這樣:
mysql_query('UPDATE users SET first_name="' . $_POST['first_name'] . '" WHERE id=1001');
登錄后復制
你可能希望生成的SQL語句為:
UPDATE users set first_name="Liz" WHERE id=1001;
登錄后復制
但是如果有個歹毒的用戶把他的名字寫成:
Liz", last_name="Lemon"; --
登錄后復制
那生成的SQL語句就變成了下面這樣:
UPDATE usersSET first_name="Liz", last_name="Lemon"; --"WHERE id=1001;
登錄后復制
現在你所有的用戶的名字都被改成了Liz Lemon, 這豈不是很悲催。
如何防止上面的事發生
一個有效預防SQL注入的方法是格式化輸入(也被稱為轉義)。你可以為每個特別的輸入轉義。或者更好的方法被稱作參數約束,這是我強烈推薦的方法,它擁有更高的安全性。使用PHP的PDO類,你的代碼現在變成下面這樣:
$db = new PDO(...);$query = $db->prepare('UPDATE users SET first_name = :first_name WHERE id = :id');$query->execute([ ':id' => 1001, ':first_name' => $_POST['first_name']]);
登錄后復制
使用約束的參數就意味著每個值都能被恰當的引用,轉義,且只匹配一個值。需要牢記的是,參數約束雖然可以保護你的SQL語句,但是卻不會在插入數據庫之后保護輸入的數據。記住,任何數據都有可能是具有破壞性的。你仍然需要剔除and/or這些容易被遺忘且稍后會展示在用戶頁面上的內容。你可以在存入數據庫的時候就進行處理,或者在展示的時候處理,但是不要省略這重要的一部。我們會在接下來的章節詳細探討。
現在你的代碼稍微多了一點,但是更安全了。你不用擔心再被另一個Little Bobby Tables弄糟你的日子。參數約束已經很帥了是吧,知道還有什么很帥么?是我!哈哈。
最佳做法和其他解決方案
存儲過程是另一個防范SQL注入的方法,它是在數據庫上編譯的。用上存儲過程以后就意味著你不太可能被SQL注入,因為你的數據一開始就沒有通過SQL語句的形式傳輸。一般來說,存儲過程是讓人苦惱的,有以下幾個主要原因:
難以測試
把業務邏輯放到了應用外(即數據庫層)
不易版本控制,因為他沒在你的代碼而是在數據庫
在需要修改這部分邏輯的時候直接限制了能勝任之人的數量(為什么存儲過程在我們日常開發中不多見?)
客戶端 Javascript并不是驗證數據的好辦法。它很容易被篡改或者被一個只具備初級互聯網知識水平的歹毒用戶回避。跟我重復:我永遠不會只依靠Javascript驗證;我永遠不會只依靠Javascript驗證。你當然可以使用Javascript來提供實時交互并做更好的用戶體驗,但是為了表達對神的真愛,還是需要在后端增加處理邏輯來保證一切都是合法的。
Mass Assignment(批量賦值)
Mass assignment是一個非常能提高開發速度的實用工具,但若使用不當也會帶來嚴重問題。
假如你有一個User的模型,你需要對它進行一些更改。可以依次更新每個字段,或者可以把所有需要修改的值一次性通過表單傳過去。
比如下面是你的表單:
登錄后復制
然后使用后端的PHP代碼處理提交的表單。如果使用Laravel框架,代碼是下面這樣:
$user = User::find(1);$user->update(Input::all());
登錄后復制
真是又快又好是吧?但是如果有個歹毒的用戶修改了表單,給了她自己管理員權限呢?
登錄后復制
我們的后端代碼就會錯誤的改變了用戶的權限。
聽起來又有一個愚蠢的問題需要解決了,但這卻是目前大多數開發者和網站可能深受其害的。最近,大家應該都聽說了一個開發者揚言Ruby on Rails很容易被這個的漏洞利用。Egor Homakov最初向Rails團隊提交問題說Rails在剛被初始化之后是不夠安全的,他這個bug很快就被關掉了。核心團隊認為要避免這個問題的方法對新手來說都是常識(attr_accessible),這個問題屬于開發者而不是Rails應該做的(大家一般都會這樣想)。Homakov覺得好心被當成了驢的某些器官,非常生氣,就黑了Github上的Rails賬號(Github是用Ra...
你是如何應對類似的攻擊呢?具體實現方法還要看你使用的框架和語言,但是你有幾個通用的選擇:
-
徹底關掉mass assignment
-
給可以安全被批量賦值的字段加白名單
-
給那些危險的加黑名單
根據你的實現方法,這些也可以被同時使用。
在Laravel框架你可以在models中添加一個$fillable變量做可被批量賦值的白名單字段:
<?phpclass User extends Eloquent { protected $table = 'users'; protected $fillable = ['first_name', 'last_name', 'email'];
登錄后復制
這樣就會在批量賦值的時候把"permissions"字段過濾。另一種方法就是增加一個$guarded變量做黑名單:
<?phpclass User extends Eloquent { protected $table = 'users'; protected $guarded = ['permissions'];
登錄后復制
哪種對你的程序方便就用哪種。
如果你不是用Laravel,你的框架可能已經集成了類似黑/白名單的方法。如果你使用的定制框架,干嘛不自己實現他呢!
類型轉換
我一般還喜歡做另外的一步,不光為安全還考慮到數據完整性,那就是對已知格式進行類型轉換。由于PHP是一門動態類型的語言, 一個值有可能是很多的類型:字符型,整形,浮點型等。通過類型轉換,我們可以確認數據都會匹配我們的所需。上面的例子中,如果我們知道變量ID一定會是整形的數字,那么它被類型轉換將變得很有意義:
prepare('UPDATE users SET first_name = :first_name WHERE id = :id'); $query->execute([ ':id' => $id, //我們知道它是個整形 ':first_name' => $_POST['first_name']]);
登錄后復制
上面這樣并沒什么意義,因為ID是我們自己定義的,我們本來就知道他是整形,但是如果ID是在別的表單傳遞過來的,它就有了讓我們心如止水的意義。
PHP提供了一系列可以轉換的類型:
<?php $var = (array) $var; $var = (binary) $var; $var = (bool) $var; $var = (boolean) $var; $var = (double) $var; $var = (float) $var; $var = (int) $var; $var = (integer) $var; $var = (object) $var; $var = (real) $var; $var = (string) $var;
登錄后復制
這不僅在處理數據庫方面很有幫助,而且在你整個應用程序上都有用。因為即使PHP是動態類型但并不意味著你不能在某些特別的地方嘗試強制類型。科學開發!
凈化輸出
輸出到瀏覽器
你不僅要關注數據的插入,還應該凈化/轉義任何由用戶生成最終會被輸出到瀏覽器的內容。
你可以在存入數據庫之前修改或轉義數據內容,或者在需要展示到瀏覽器的時候做這件事。這通常要看你的數據是如何編輯以及它的用處。比如,如果用戶可能再次編輯,原樣存儲,凈化輸出將更有意義。
什么時候轉義會輸出的用戶內容有大的安全意義呢?假如用戶提交了下面這樣的Javascript片段到你的應用,等下還可能被顯示到瀏覽器:
登錄后復制
如果在輸出的時候你什么都不做,這個惡毒的Javascript通常都會像你自己寫的一樣被執行。這只是一個無所謂的alert(), 但是黑客一般都不是這么友善。任何可能被顯示且從外面插入到你應用的數據,你都要對其處理。
還有一個這方面的應用是圖片的XIFF數據。如果你的應用會展示一個用戶上傳的圖片的XIFF數據,這也應該被格式化。
如果你正使用模板引擎或者你使用的框架提供了模板機制,它可能已經自動完成了轉義,或者它會提供一個相關的方法給你。詳細的使用和實現你可以查看相關的文檔。
這完全取決于你,PHP內置了很多函數,當你要在瀏覽器顯示數據時都會成為你最好的朋友: htmlentities()和htmlspecialchars(). 他們都可以轉義使得在展示之前就變得更加安全。
htmlspecialchars(); 可以完成90%你想要的結果。他會自動轉義 (如, &) 這些特殊的字符至HTML實體。
htmlentities()和htmlspecialchars()性質相差無幾. 如果可能他會轉義所有的字符到HTML實體。這在很多時候就不太實用。確認這些方法使用之后的結果,之后再評估哪種是你想要的。
命令行的響應
別忘了對命令行腳本的運行結果進行格式化。相關的函數有escapeshellcmd()和escapeshellarg()
顧名思義。使用escapeshellcmd()轉義所有命令。可以防止所有命令被執行。escapeshellarg()用來搞封裝的參數來確保他們被正確的轉義,確保不會用你的應用程序來處理命令一樣處理參數。
第二章 - HTTPS/SSL/BCA/JWH/SHA 等其他的隨機序列及他們的實際問題。
又到了小故事時間。2010年10月Eric Butler發布了一個叫做Firesheep的火狐瀏覽器擴展,是為了展示一個在網頁上人們還沒有注意到的大問題。Firesheep 可以讓一個普通的線上用戶在本地網絡輕易的看到未加密的通信信息,進而劫持到其他用戶的會話。這個擴展功能允許人們以Cookie的方式查看在公共網絡上交換的信息,在中間攻擊,劫持。很害怕吧,這確實是真的。可能你在想,這只是猜想吧。那么實際點。讓我們一起來用實例證明它。
在2010年12月,Jane因為工作出差住在希爾頓酒店,這時候John也在。他們都在為了人生目標而奮斗。Jane之前再新聞上聽說了Firesheep覺得好玩。淘氣的他連上酒店的wifi并打開了Firesheep。恰好,John正在使用wifi,Jane看到John正在使用一個不安全的連接訪問公司的網頁郵箱。一次點擊John的郵箱賬號就被拿到了。想想這可以造成多大的麻煩,私密的郵箱賬戶,郵箱的常用功能都打開了大門。
像這種利用未加密的網絡通信的會話劫持(又名sidejacking),是一直有可能被知道其他人在做什么的。隨著Firesheep的發布大家都知道了下個插件點個鼠標就能干很危險的事。
你下載了Firesheep,(我知道你這個壞小子在干嘛)你可能在想可怕地事即將發生。實際上大相徑庭,這些互聯網公司早已動身完成了這次保護并且嚴肅的使用上了HTTPS。Gmail, Facebook, 以及 Twitter均已整站采用HTTPS。之前只是用在了登錄頁面的情況通過上面所說的那個例子已經證明可以暴露用戶會話了。
什么是 HTTPS
普通的網頁流量基于HTTP協議傳輸,當你在瀏覽器地址欄敲下 "http://www.google.com" 的時候你就在使用HTTP,協議不是擺在最前面么。HTTP協議通過80端口傳輸,HTTPS是443端口。HTTP一點也不安全,他可以把任何消息清澈透明的發送給正在竊聽的任何人。HTTP的意思是"HTTP Secure" 或者 "HTTP on SSL”,到底是哪個還有爭議,但是他們都在說明一件事。HTTP使用SSL進行安全傳輸。
我們這里只講HTTPS的宏觀工作原理,因為細節對大多數人來說并不用關心。如果你想了解更多,谷歌是個好地方,當然如果你訪問不了用百度也行,呵呵。
能說明SSL工作原理的真實故事是外交郵袋。它的內容是安全的,因為它只有在最終被傳遞到手握有效證書的人那里才能被打開。這個袋子被國際法律保護著,同理,SSL的加密信息被健壯的代碼和密鑰保護。
證書權威局會注冊你網站的證書并證實有效。用戶瀏覽器能夠識別主要認證機構并且會拿網站的證書和證書權威局提供的根證書校驗。通信在雙方都會用這個密鑰加密,所以所有的通信都被加密。如果你曾使用過通過公鑰無密碼SSH登錄你應該比較熟悉這個過程了。你有一個公鑰和一個私鑰用來使遠程機器完成身份驗證。
如果你整站采用了HTTPS這就有效預防了中間攻擊,還包括我們上面提到的會話劫持。
局限性
使用HTTPS也會在有些情況有局限。
虛擬主機
普通的虛擬主機配置不能使用在SSL上。如果你使用托管虛擬主機或者在一個服務器跑了多個站點都會造成問題。因為服務器只有在連接建立之后才能確定帶有SSL身份驗證的請求頭。因為證書只能有一個host,這就是說他不是那么簡單生效。最簡單的辦法就是使用配置多IP地址并使用IP綁定hosts來代替你可能正在使用的域名綁定host。我們通常推薦為安全站點配置單獨的服務器,如果你想使用HTTPS那么你可能還需要一臺專用服務器。
當然還有一些主機服務商把一個共享證書用在他們服務的一系列網站中。這能讓你更快速便宜的用上HTTPS。問題是你域名必須使用人家提供給你的,例如
https://yourApp.com/login
登錄后復制
可能會變成下面這樣
https://yourHost.com/yourApp/login
登錄后復制
你是不是關注這個問題取決于你的應用和品牌需要。
速度
HTTPS連接包含SSL握手來建立連接,因此使速度變得慢了些。一次初始握手過程附加的執行連接包含了內容的加密和解密,意味著一旦初始連接完成,隨后的連接就不會太慢了。性能影響非常不值得去思考,這并不是放棄HTTPS的正當理由。
緩存
切達干酪。脂肪堆積。已故的總統。現金。算了,其實我們在聊緩存。可以加快加載時間的秘密武器。你必須用英國口音說說。現代瀏覽器會像HTTP一樣緩存HTTPS內容。為了讓老瀏覽器支持只需要使用Cache-Control 首部, 比如
header('Cache-Control: max-age=31536000');
登錄后復制
就會告訴瀏覽器我要緩存一年!
真正的問題在代理緩存。代理緩存可能來源于網絡提供商或者一個打算提高連接速度的服務。這主要用在網絡速度比較慢的農村。使用HTTPS,這種緩存是不可能的因為在代理看起來所有的通信都是加密的。對大多數站點來說這不是一個主要問題除非你有一個相當廣闊的用戶群,或者一個把目標用戶定個偏遠地區的應用,是應該深思熟慮的。
還有一件事,好運的是網站的大多數地方并無需緩存。這意味著你不需要讓瀏覽器緩存任何內容,坐下來擬定哪些應該被緩存和緩存多久。例如,CSS和JavaScript文件應該被緩存比較長的時間,而用戶的信息流應該頻繁更新。
證書類型
目前存在有兩種SSL證書.
域名驗證證書驗證并沒那么嚴格但足夠便宜。50刀起,這是小站最好的選擇了。對用戶來說通常二者的區別是域名驗證證書只會顯示一個鎖的圖標,而擴展驗證證書就會顯示整個綠色的地址條。
擴展驗證證書是SSL證書的高端標準。他不僅會驗證域名的主人還會驗證域名主人的是否合法。因為這通常包含了證書權威局認證的個人成就的一部分,這些證書也是大大的昂貴的。通常價格是500刀起。這將是那些卓越大公司的證書首選。瀏覽器在使用這種證書的時候會顯示出一整個綠色的地址條,讓用戶看到就會心如止水,信任感極強。
什么時候用HTTPS
通常看到的情況是整站使用HTTPS或者只在敏感數據的地方使用。多年來我們經常看到登錄頁和購物車頁會被加密。他們仍然在必須的地方加密但是會在中間攻擊時把用戶會話暴露給黑客。最近比較流行整站使用HTTPS。用來說明你站點的任何地方都已被加密。這在很多時候都不錯,HTTPS的局限應該被仔細想好,不要盲目的在沒評估過流量的時候整站使用HTTPS。如果你已經確定想好你的應用在使用HTTPS后他的局限性小于加強安全性,那么全站使用HTTPS是非常推薦的。
此時此刻你是不是覺得很容易就會忘掉整個HTTPS事件呢?呵呵,好吧,讓我們慢慢來。不管你還有什么限制你都有責任為你的用戶實現一切你能實現的所有安全策略。如果你正在跑的程序比如是購物車或者收集信用卡信息,那么毋庸置疑,一定要采用HTTPS。即使有些還不能確定是絕對的敏感數據,若有賬號相關也應該被加密保護起來了,別再擱置了,只要需要就把HTTPS用起來吧。
實現 HTTPS
我需要哪種SSL證書?
主要問題是你是否需要用在子域名上。如果你需要用在很多子域名,如
api.yourApp.comdocs.yourApp.comyourMom.yourApp.comcart.yourApp.com
登錄后復制
你就需要通配SSL證書了。如果你只是用在主域名,如
yourApp.com
登錄后復制
那么非通配的標準版就足以應付了,用通配那個版本的好處就是當你之后需要的他的時候直接就有,節省成本。
生成服務器證書
為了讓證書頒發機構簽署并生成你的證書你需要在服務器上生成密鑰然后上傳這些到證書頒發機構。
對了,還需要OpenSSL,如果你服務器上還沒有,你應該安裝上它。在各種操作系統上安裝軟件的教程不在本書的講述范圍,網上有大把的教程,希望你可以完成在你服務器上的配置HTTPS的任務。如果你不會那找個人來幫你是個不錯的選擇。
第一步先建個目錄來存儲密鑰,可以隨意找你喜歡的目錄比如
/usr/bin/ssl/
登錄后復制
我們來生成私鑰
$ openssl genrsa -out yourApp.key 1024
登錄后復制
然后使用私鑰生成簽名
$ openssl req -new -key yourApp.key -out yourApp.csr
登錄后復制
過程中一般都默認即可,一個需要注意的地方是"Common Name",這里寫你的域名,如"yourApp.com"。
這時候我們得到兩個文件
'' /usr/bin/ssl/yourApp.key "
'' /usr/bin/ssl/yourApp.csr "
做其他事之前,我們先備份一下key文件,然后再備份一次到其他地方。因為如果丟了你就要重新購買新的證書了,當然這時候服務器也會崩潰。
獲取SSL證書
開始用起HTTPS的第一步是獲取證書。一些證書頒發機構提供了一些便宜甚至免費的證書,但是他們不會預裝在流行的瀏覽器使得對于面向公眾的站點失效。如果是內部應用就可以使用那些免費的然后自簽名使用,如果是其他的還是購買為妥。
首先我推薦你看下你的DNS提供商是否有一些折扣的或者易配置的證書,比如我用的DNSimple,他們就有大的折扣。
如果你的DNS提供商沒有證書服務,Symantec/VeriSign是不錯的證書頒發機構。
現在支付吧。
然后就根據你選擇的證書頒發機構提供的步驟來吧,通常你只要上傳你的簽名(yourApp.csr)就哦了,他們就會給你發郵件來注冊證書。
你的證書頒發機構會給你一個簽名過的證書比如yourAppSigned.crt。上傳到你的服務器,比如
/usr/bin/ssl/yourAppSigned.crt
登錄后復制
Apache 配置
如果你使用Apache可以參考下面的步驟,如果是用其他的請跳過本小節。打開httpd.conf文件。注意,有些發行版為https使用了分離的配置文件。比如我現在使用的OSX版本就是用的httpd-ssl.conf文件。
添加一個如下的虛擬主機,它可能和你HTTP站點的長的類似
DocumentRoot "/path/to/your/app/htdocs" ServerName yourApp.com SSLEngine on SSLCertificateFile /usr/bin/ssl/yourAppSigned.crt SSLCertificateKeyFile /usr/bin/ssl/yourApp.key
登錄后復制
重啟 Apache
$ apachectrl restart
登錄后復制
or
$ service apache restart
登錄后復制
通常這時候訪問"https://yourApp.com”,你發現已經搞定了!
Nginx 配置
如果你的服務器是NGINX可以參考下面的步驟,如果你再使用其他的,不好意思了,篇幅有限……自己搜搜吧。呵呵!
打開Nginx的虛擬主機配置文件,一樣添加一個類似的配置
server { listen 443; server_name yourApp.com; location / { root /path/to/your/app/htdocs; index index.php; } ssl on; ssl_certificate /usr/bin/ssl/yourAppSigned.crt ssl_certificate_key /usr/bin/ssl/yourApp.key}
登錄后復制
重啟 Nginx
$ sudo /etc/init.d/nginx restart
登錄后復制
or
$ service nginx restart
登錄后復制
訪問吧"https://yourApp.com”,哦了!
小附加內容
Apache的最好資源就是他的官方文檔
http://httpd.apache.org/docs/current/ssl/ssl_howto.html
登錄后復制
NGINX 的 WIKI 也是一個不錯的開始
http://wiki.nginx.org/HttpSslModule
登錄后復制
其他的服務器?把下面網址中的yourWebServerName換成你的服務器名稱,敲在瀏覽器的地址欄,奇跡即將發生。
http://lmgtfy.com/?q=yourWebServerName+SSL+certificate+setup
登錄后復制
路徑
絕對路徑
你既然已經搭好了HTTPS環境,那么用戶訪問的時候你應該在需要的時候讓他們訪問到HTTPS版本。Apache/Nginx 的重定向都能做到。另一個簡單的做法是直接把絕對路徑配到你的HTTPS網址上,比如"https://yourApp.com",然后強制把HTTP的鏈接重定向過來。
很多時候你希望某些頁面使用HTTP而其他的使用HTTPS,這時候正確的服務器配置及合適的代碼路由設置就成了關鍵。
相對路徑
另一個需要說的不是我們這節安全相關的重點但可以在你同時使用HTTPS和HTTP的時候讓你的生活變簡單。資源的網址,如CSS,JS可以用兩個反斜杠來替代http:// 或 https:// 來正確的適配協議。比如你可能在主頁有下面的代碼
登錄后復制
訪問 https://yourApp.com 的時候會加載
如https://yourApp.com/assets/main.css
如果訪問 http://yourApp.com 則會聰明的加載
如http://yourApp.com/assets/main.css
這雖然是一個小技巧,但是非常實用,其實,我們都關心,不是么?
搞定
好樣的,我們都搞定了。摸摸頭,拍拍背,喝點喜歡的飲料,小憩一會,我們再看下一章。
第三章 - 為每個人存儲加密的密碼
你現在應該已經知道了我們要說的內容了。克里斯是Marvel Comics web開發組的一個初級開發者。又是一個變態的伯班克午后的酷暑。在他們團隊最近的漫畫門戶項目中克里斯負責開發登錄模塊。他的"團隊"的意思是說他和其他開發者,克里斯忘了抹清涼油,今兒可真熱啊。
克里斯已經構思好了登錄系統的工作原理。大家期望的標準流程都有,包括登錄,登出,忘記密碼等一系列……他還需要存儲密碼以便在登錄時驗證,并在找回密碼的時候以便發送給用戶。一會兒的時間他想過了所有的登錄流程并開始思考用戶在數據庫訪問密碼時的安全問題。他知道他應該對密碼加密但是如何等登錄的時候解密?在找回密碼的時候又該如何?
經過漫長的枯燥45分鐘的研究克里斯決定使用PHP的兩個函數mcrypt_encrypt()和mcrypt_decrypt(),克里斯振奮起來,密碼的加密解密的安全問題已經都使用PHP搞定。項目馬上就要做完了,他還有一半的時間。
但是朋友(我們是朋友了對吧?),當你再Marvel.com重置密碼的時候你的郵箱收到了你之前的明文密碼。在一封郵件里!有你的明文密碼!你現在看起來需要改一個你在任何地方都沒有用過的密碼了。然后我默默的等待。默默的在街角哭泣,想著這會被如何利用,想著人們的苦難,何時才能后天下之樂而樂。
這個故事的中心思想是,不要存儲密碼。只存儲密碼的不可逆哈希串。讓我們來看看如何正確的做這件事吧。
小說明
我并不是密碼專家。這些都是我從經驗得到的個人建議。這些都是主觀的最佳做法但并不意味著可以為安全存儲*發射代碼作為說明書。你最重要的推特消息都會保持安全的。
哈希是什么?
我們第一把覆蓋到概念了。散列法并不是加密,密碼的散列是不可逆的。這意味著不能被解密。其實并沒有像用戶/管理員/老媽/任何人展示密碼的需求。一旦密碼被輸入它就變成了一個唯一哈希序列,而只能由輸入的原始密碼散列計算出。
流行的攻擊
在進一步討論之前,我們先來深入探討下哈希算法的流行攻擊。
查找表
查找表就是根據已知密碼查找散列序列的對應關系表。簡單表述如下
password | hash------------------------pass1 | bidfb2enkjnfpass2 | psdfnojn3nodetc...
登錄后復制
這就是等下要和你數據庫中的密碼散列值對比的數據用來確定是否恰好“碰到”。這種攻擊在你使用加鹽的隨機散列的時候是幾乎沒用的但是如果散列不加鹽當然就要比搶劫火車容易多了。
彩虹表
彩虹表在技術復雜度上和查找表很像。他基于數學方法用較小內存實現了查找表。我們在這里提到的你都可以互換理解。彩虹表在面對加鹽的隨機序列時也不好用所以也與現代哈希算法不太相關。
彩虹表非常復雜,超出了本書的范疇。如果你想了解的更多,請參考 [original paper published by Martin Hellman] 介紹了它的概念. [Wikipedia article on Rainbow Tables] 也是一種比較容易消化的學習資源。
碰撞攻擊
碰撞攻擊是大多數哈希算法的主要安全漏洞。有兩種碰撞攻擊類型。
經典的碰撞攻擊是在兩個不同的字符串生成了一樣的哈希,例如
string1 = 'abc123'string2 = 'bcd234'hash(string1) === hash(string2)
登錄后復制
前綴選擇碰撞攻擊是兩個不同的前綴組成的兩個字符串生成了同樣的哈希,例如
prefix1 = 'zxy'prefix2 = 'abc'string1 = 'abc123'string2 = 'bcd234'hash(prefix1 + string1) === hash(prefix2 + string2)
登錄后復制
來點鹽
當然,這還遠遠不夠。下面的問題是查找表以及彩虹表的漏洞利用。這些漏洞都是基于一個保存了流行密碼哈希序列的一張表。彩虹表稍微復雜一些但是還是可以搞定的。我們怎么預防呢?鹽,隨機鹽。
鹽是為了使哈希唯一而附加在其上的東西。鹽也可以被添加到瑪格麗塔的邊緣,使之更加美味。你就可以用一個隨機字符串(鹽)和文本密碼拼在一起來得到一個唯一的值。這意味著即使有了密碼哈希表,攻擊者也不能正確的匹配上因為你使用的隨機鹽。只有兩個完全相同的密碼才會生成一樣的哈希。隨機鹽是保障密碼安全非常重要的一部分。
隨機的也不一定總是隨機
你加的鹽只有隨機才會生效。盡管隨機!==隨機。你不一定需要真正意義的隨機數,rand()函數也不會使之竹籃打水。
使用PHP的內置函數rand()或mt_rand()的問題在于可以被種子數據操作和決定,而不夠"隨機"。生成一個真正隨機數的重要組成部分是在源頭中填充足夠的熵。熵是由生成隨機數的系統集合的真正隨機信息的總量。大多數非加密隨機數生成器,像rand()和mt_rand(),是使用算法生成數字而沒有足夠的外部數據使之真正唯一。這意味著rand()產生的數據可以被攻擊者猜測并操作。通過認真觀察rand()的輸出攻擊者可以敏銳的判斷出規律并預測。實際上只需要rand()的624個值就足以預判后面的所有值了。
你已經被警告過了,不應該使用rand()作為鹽。當你購買這本書的時候我們就已經在此約定,你不會因為你使用rand()作為鹽的時候我用第一代iPad扇你的臉。比板磚還帶勁。
問你服務器的/dev/random在大多數系統中是真隨機的最好辦法。缺陷是當把它用在登錄模塊時會在收集系統熵的時候阻塞。收集熵時也會收集系統的環境數據;像各種硬件數據,鍵盤輸入,鼠標移動,硬盤的訪問,等。為了生成一個隨機字節的緩沖區。這會消耗較長的時間來返回結果,尤其在系統不忙的時候。我最近正好碰到了這個問題,我們團隊的一個小哥正使用/dev/random生成一個激活碼。一切看起來都那么美好,過了測試。但當我們把它部署到生產機器時,響應需要60秒。服務器壓力并不大因為只有這個項目在跑,導致/dev/random阻塞的時候正在收集熵。咋解決咧?/dev/urandom;
/dev/urandom在真正隨機上并不健壯但它卻是密碼安全的。也就是說它并不是真正隨機數但被認為在用作鹽商足夠安全。/dev/urandom能不阻塞的馬上返回一個非常不錯的偽隨機數。/dev/urandom使用已有的熵池來生成偽馬上返回一個非常不錯的偽隨機數。/dev/urandom使用已有的熵池來生成偽隨機數,在大多數身份驗證系統中都足夠安全。如果你正在為一個*發射系統寫登錄代碼最好還是讓用戶在/dev/random旁邊等待,但如果用在社交照片分享站點我想/dev/urandom就已經很不錯了。
哈希算法
讓我們一起討論下流行的哈希算法。
MD5
沒什么比不恰當的使用MD5算法被我們熟知了。通常是因為大多數數據庫都缺省支持。MD5早已被數學方法證明其并不安全。它的缺陷是在現代硬件上非常容易產生沖突。
有一個不容忽視的例子,在2005年研究員們能夠使用筆記本電腦在MD5校驗和產生沖突。這說明并不需要$200k的性能怪獸服務器就能破解MD5,只需要2005年代的一臺筆記本。對互聯網來說就像100年以前。別再使用MD5做密碼的哈希了。這是在用不安全的哈希來驗證數據內容,當然,這也會讓攻擊者更有興趣。
MD5也不是糟糕的一點不能用因為多數情況使用合適的鹽還是足夠安全的。但這也不是告訴你不應該為未來尋找更好的解決方案。
SHA-1
古老的SHA-1加密,多年來一直被認為是安全和可信任的。在互聯網也存在了數十年。在2005年(2005在安全上真是悲催的一年)山東大學的科學家發布了一個研究報告,SHA-1算法可以通過不到2^69^次哈希就產生了沖突。2^80^次哈希操作時產生的沖突被認為是密碼安全的。參考下這個,2^80^差不多是1.20892x10^24^,因此"密碼安全"看起來是還算安全的。但后來摩爾定律又證明了SHA-1并不安全而且不應該使用在需要保證絕對安全的應用中使用。
就像上面說到的MD5,當我們使用隨機鹽的時候SHA-1在算法上也是安全的。
SHA-256 / SHA-512
在2001年SHA-2被當做SHA-1的迭代版本。在2005年SHA-1被證明不安全之后它被慢慢接受。SHA-256和SHA-512一個道理;SHA-256使用32位字符,而SHA-512是64位。他們的循環次數也不同。但核心算法幾乎是完全一樣的。
SHA-2目前被認為是密碼安全的,在使用足夠的循環之后(>64)仍沒有發現漏洞。
由于沒有被Blowfish大量的審查,BCrypt只在內部使用。Blowfish加密自1991年以來一直被認為是安全的,但使用它弱鍵是一個已知的缺陷。它是基于一個加密算法給BCrypt帶來了一層額外的開銷,使它優于標準的哈希算法,換句話說BCrypt設計的較慢。但慢是一件好事!
在這我推薦BCrypt, SHA-256 或者 SHA-512是可以作為目前有效的安全備選的。可以用來作為衍生算法的一部分,比如PBKDF2或用PHP的crypt()函數實現。
BCrypt
大多數人覺得BCrypt是一個新丁而沒有被廣泛的使用。其實它是在1999年發布的,這明顯是老頭兒了。BCrypt是基于Blowfish密碼的衍生方法,它是迭代的所以由于生成哈希的開銷關系他可以防止暴力破解。
盡管事實上很多密碼學家都在大量關注BCrypt但目前仍還有公布的漏洞。一直到寫這篇文章的這么長時間以來(2014),BCrypt被認為是密碼安全的。
BCrypt在加密純文本密碼的時候有72字符的限制。所以經常有切掉多余字符的做法或者只支持最大72個字符的驗證。
BCrypt也是我們后面例子中選擇的密碼加密方案。
SCrypt
SCrypt是個新成員。在2012年發布,它是一個在內存方面加強的衍生算法。理論上來說SCrypt在高內存消耗條件下是個更安全的算法。但是它太新了我個人不太推薦在現在使用。
在密碼界新的一般都不太受歡迎,因為它并沒有接受足夠的像那些老算法那樣相同等級的攻擊和審查。雖然最近爆了一些漏洞但這并不意味著SCrypt不夠安全,但還是要遠離理論上的安全優勢。
值得注意的是SCrypt已經被很多流行的加密貨幣使用到他們的采礦業務,顯著的有萊特幣和狗狗幣。這意味著它也會像它的前輩一樣受到大量注意。
存儲
這節會比較短,嗨起來!不管你使用什么系統存儲你的密碼哈希,不論使用關系數據庫,密鑰存儲器,鎖箱,襪子抽屜,或者文件系統;使用不限長度的文本字段或者我推薦使用varchar(255)。你的哈希算法會生成最大長度的字符串所以你并不用為攻擊者脫你的數據庫而擔心。不同的哈希算法會生成不同長度的字符串所以你要根據你的算法來設置字段長度。我比較喜歡設置較長的字段以防之后用別的哈希算法,而不摳門在那多出的存儲空間。
使用BCrypt你的哈希最大是60個字符。所以理論上來說你可以設置一個varchar(60)的字段但并不兼容之后的修改。為了你的密碼還是別在乎那幾個字節了,搞個text字段或者用varchar(255)吧,你值得擁有。
驗證
唯一需要驗證的是密碼的最小長度。你應該允許使用字符,空格,短語等,你的用戶可以設置他們想要的任何密碼。好的短語是比較推薦的,"correct horse battery staple”總比"myNewPassword”要強。你只需要擔心密碼不夠復雜,不夠長。
為了給所有人你的愛,別做像通過Javascript限制復制粘貼的傻事。如果人家用戶使用密碼管理工具你應該讓其用的更爽。如果你這么做了,你的用戶會覺得很不爽,罵聲當然就來了。
有個需要注意的是BCrypt只能使密碼的前72位有效所以你可以從技術上進行約束。這雖然會稍微有些限制你的用戶但是對你未來選擇其他哈希算法帶來了方便。如果你的用戶對一個74個字符的密碼記得特熟練,你可以只使用它的前72位,總好過讓他想個新的出來。有些代碼會推薦先使用標準哈希算法(SHA-256, SHA-512, 等)然后再使用BCrypt對其哈希結果再做一個來解決這個長度問題。這真是一個有夠完美的解決方案,但我不太推薦,因為72個字符加上一個有效的鹽足以讓你的密碼在現代科學上有夠安全了。
放在一起說說
我們剛說了一些基礎知識,現在我們一起敲點代碼吧。
開始我會帶你用傳統的/棄用的做法隨后我也會帶你使用PHP 5.5或更高版本的新方法來搞這些。原因有下:
我希望你能夠了解底層原理而不是只看到一個封裝的方法
你可能正在使用一個低版本的PHP,但是我還不想放棄你
下面的場景是注冊新用戶。我們需要生成一個隨機鹽,做他們的密碼哈希,然后把他們都存在一個虛擬的數據庫中。作為未來的身份驗證使用。
< PHP 5.5
如果你還在用低于5.3.7版本的PHP那么你至少要為了保證最起碼的安全升級到5.3.7來。如果你實在不升我不知道還能推薦給你什么。升級之后你會發現很多老版本的bug都被修復了。在我們的場景中,BCrypt在5.3.7之前就有缺陷。我們會在下面的例子中使用$2y$前綴,他會一直作為前綴參數。意思就是說他已經更新了漏洞修復和新的邏輯。
第一步我們要先生成一個隨機鹽。根據你系統安裝的擴展使用PHP你有很多方法去做這件事。比如:
<?php//獲取二進制隨機鹽$saltLength = 22;$binarySalt = mcrypt_create_iv( $saltLength, MCRYPT_DEV_URANDOM);//把上面的數據變成安全字符串$salt = substr( strtr( base64_encode($binarySalt), '+', '.' ), 0, $saltLength );
登錄后復制
我們使用了MCRYPT_DEV_URANDOM參數的 mcrypt_create_iv()函數,這樣就可以告訴它從/dev/urandom緩沖區取內容。封裝在里面的bin2hex()讓我們可以得到一個十六進制字符串來替代二進制數據。
我們使用了MCRYPT_DEV_URANDOM參數的 mcrypt_create_iv()函數,這樣就可以告訴它從/dev/urandom緩沖區取內容。封裝在里面的bin2hex()讓我們可以得到一個十六進制字符串來替代二進制數據。
<?php//獲取二進制隨機鹽$saltLength = 22;$binarySalt = file_get_contents( '/dev/urandom', false, null, 0, $saltLength);//把上面的數據變成安全字符串$salt = substr( strtr( base64_encode($binarySalt), '+', '.' ), 0, $saltLength);
登錄后復制
現在我們已經得到了鹽,下面我們對密碼做哈希
user()->create(array( 'password' => $passwordHash ));}else { //錯誤提示}
登錄后復制
現在我們已經給密碼做了哈希并存入了數據庫。注意我們為什么沒有存鹽呢?是因為crypt()會保存返回的哈希中我們選擇的算法,密碼的哈希值,以及鹽。
我們已經有注冊用戶了,比如他要登出站點(好大的膽子!)然后一會又回來了,他們需要登錄。登錄是簡單美好的,我們只需要驗證他的文本密碼和我們存在數據庫中的哈希。
user()-where(array( 'email' => $_POST['email']))->row(); //驗證他們是否相等 $pass = $_POST['password']; if (crypt($pass, $user->pass) === TRUE) { $valid = TRUE; }//其他的驗證,錯誤處理等等... if ($valid === TRUE) { //驗證有效}
登錄后復制
如果我們把之前生成的哈希放在crypt()的第二個參數他將自動選擇算法并使用相應的鹽來生成哈希與存在數據庫中的密碼哈希進行對比。
>= PHP 5.5
在PHP5.5.后引進了新的密碼哈希函數使得很大程度簡化了密碼操作流程。目的在于隱藏那些復雜的邏輯并使PHP開發者使用默認的函數就能保證安全而不依賴于所有的開發者都需要掌握這些密碼知識,換句話說他們正在努力使我的書越來越不好賣,尷尬。
嚴肅的說,應該盡可能的使用PHP的密碼函數。它提供了內置的最新版本哈希并加帶附加的安全檢查而我們這里并沒有包含,比如保護定時攻擊(Timing Attacks).
我們下面就講一下PHP 5.5語法的步驟。我們將用到password_hash函數來生成密碼的哈希,這個函數會自動創建隨機鹽所以我們會省一些步驟。我們看下代碼:
user()->create(array( 'password' => $passwordHash));
登錄后復制
下面是在登錄的時候驗證密碼
user()-where([ 'email' => $_POST['email']])->row(); $pass = $_POST['password']; if (password_verify($pass, $user->pass) === TRUE) { $valid = TRUE; }if ($valid === TRUE) { //附加邏輯}
登錄后復制
像上面你看到的那樣,PHP 5.5中的新密碼函數可以讓你的生活簡單清爽。這個函數的另一個優點是它內置于PHP所以你在未來可以一直依賴它,而不是自己持續的追最新密碼算法的版本。這很吸引人對吧,把它的利弊交給PHP核心團隊,留更多的時間讓自己去做其他更精彩的事情。
當你想要實現身份驗證模塊的時候確保參考PHP文檔來保證代碼的時效性。當然,也應該知道其他有效密碼函數的優勢比如password_get_info()和password_needs_rehash().
你還在用PHP 5.3么?錯過了新版5.5的所有樂趣?別急,Anthony Ferrara幫你做了一些事情。他的password_compat庫在PHP 5.3.7實現了新的密碼函數,你可以從這里下載[Anthony's Github]
我極力推薦你使用這個庫而不是自己寫,因為它已經被充分測試過了我們完全可以信賴它。
殘忍的暴力攻擊防護
你可以擁有最完美的密碼哈希,用了所有的安全措施,但是當殘忍的攻擊者不斷的攻擊你的登陸頁直到他獲得正確密碼的做法是沒有用的。暴力攻擊就是攻擊者使用軟件不斷的嘗試不同的密碼直到他登錄為止。
防護這種攻擊很簡單,遺憾的是我發現很多站點并沒有做的很好。所以我們該如何做好呢?那就是當有人以這種方法獲取密碼的時候就讓他靠邊等著去吧。
一個人試著登錄,失敗了。然后又重試,又失敗了。再來,呵呵。這時候你就讓他靠邊等一分鐘再來。好了,這就是最簡單的解決辦法,也是最有效的辦法。如果一個攻擊者每分鐘才能試三次密碼那么他將等到天荒地老,還攻擊什么呢。你的用戶安全了。不一定是限制三次,三、五、十都不錯;我個人推薦每分鐘不超過10次。
你還可以擴展一下,限制IP。只允許某一IP登錄所有用戶X次而不是只限制登錄一個用戶X次。還要確保包含恰當的錯誤提示以及合理的重試時間,企業團體用戶一般都會拋出一個公共IP地址所以你還不能因為一個壞人而懲罰數千個合法的用戶。
當然,把相同的邏輯用在API驗證上,讓攻擊者認識到不同的地方也會是一樣的結果。不管你在前端用了什么安全措施,也要把他合理的移植到API上去。
升級遺留的老系統
現在房間里有個大象。抓住他?大象哎。一晚上也抓不到吧。那么我們如何升級在使用MD5密碼并沒加鹽的原有系統呢?
我可以給你兩個選擇:
1 - 在每個用戶登錄的時候默默的幫他用BCrypt升級他的密碼。他們在什么都不知道的時候你的數據庫就已經更新到了安全的密碼。
2 - 使用BCrypt在原有的MD5串上再做一次哈希。新密碼就是MD5和BCrypt的二重加密了。
第一種升級辦法
第一種方法是遷移新身份驗證方案的傳統建議。而且在大多數情況下是最好的辦法。你可以像下面這樣做來實現:
user()-where([ 'email' => $_POST['email']])->row(); if ($user->pass_hash_algo === 'bcrypt') { //他們已經登錄過并已經完成了升級,我們可以驗證登錄 }else { //驗證一下老密碼 $oldHash = md5($_POST['password']); if ($oldHash === $user->pass) { //用原密碼生成一個新的哈希值 $newHash = password_hash($_POST['password']); //保存到數據庫并標記完成 $user->pass = $newHash; $user->pass_hash_upgraded = 'bcrypt'; $user->save(); $valid = TRUE; } //...}
登錄后復制
這樣改一下登錄邏輯你的用戶將在登錄的時候自動升級他密碼的新哈希。積年之后這次升級將輕松的用上了我們推薦的哈希算法。之后SCrypt可能會變成被接受的標準那時候你的升級將輕松易得。
第二種升級辦法
第二種辦法雖然解決的不清澈但是一經使用立刻生效。上面的第一種方法在一段時間還會保留一些不安全的哈希密碼直到所有的用戶都登錄過但這種辦法一旦上線立刻能使你的數據安全,你不會丟失任何一夜的睡眠。這節你會直接在所有的密碼哈希上使用BCrypt再做一次哈希。你的最新哈希是在MD5基礎上使用了BCrypt之后的強大哈希。這增加了復雜度因為你每次都需要同時使用BCrypt和MD5直到永遠,會笨重很多年。
一共包括兩步,第一步寫個類似下面的腳本掃一遍你的現有數據庫。告訴我你會用導出的數據庫而不是在生產服務器上干這件事。對吧?因為那會讓我們都傷心…你還要在每條記錄上加一個狀態。下面是個例子,別復制粘貼啊,咳咳...
users()->result(); foreach ($users as $user) { $newHash = password_hash($user->pass); $user->pass = $newHash; $user->save(); }
登錄后復制
現在數據庫已經升級了我們還要改下注冊方法
user()->create([ 'password' => $passHash]);
登錄后復制
當然登錄方法也要同樣的將校驗文本密碼改成驗證MD5密碼
user()-where([ 'email' => $_POST['email']])->row(); //在使用bcrypt之前先使用md5方法 $md5Pass = md5($_POST['password']); //驗證密碼的正確性 if (password_verify($md5Pass, $user->pass) === TRUE) { $valid = TRUE; }//其他的驗證等
登錄后復制
好了,我們終于安全了
這就是本節的全部了。雖然還有更多關于密碼的知識但你必須知道的已經在這里了,我可能不會寫密碼的附加章節或者高階內容了。敬請期待下一章吧,我們會講URL的限制,安全的文件操作,確保沒人能看到你的數據。
資源
如果你對本章的密碼學感興趣我強烈建議你看下Bruce Schneider的研究成果。你應該讀一下他的書Applied Cryptography,在加密方面很權威。
我還喜歡關注PHP社區其他成員的工作,Anthony Ferrara倡導從PHP5.5內置密碼哈希函數。這使得我們在未來缺省構建安全的PHP應用方向邁了一大步。
第四章 - 身份驗證, 權限控制, 安全的文件操作
Erica在一個小型的制造業公司做程序員以及IT支持。他最近被分配開發公司的員工系統,帶領公司從紙質時代飛到數字世紀。
第一步是讓員工掃描文檔,用電子存儲替代紙質表格。
歸檔系統開發的一切順利,系統由所有文檔組成的數據庫清單構建,還能過濾像部門,日期,標簽這些。沒什么復雜邏輯,Erica就想保持簡單。
文檔會以用戶的部門和職位為過濾條件呈現給用戶(比如,某人,管理部,主管)。幾個月以來都相安無事,但有一天還是迎來了不快。一個機密文件出現了漏洞。經過調查發現一個不滿的員工得到了一個只有高級管理員能瀏覽的文件訪問權。這個員工把此文件泄露給了競爭公司聽說輕而易舉的拿到了競標。
這是怎么發生的呢?Erica花了一個周末去研究,突然,他發現了,這是多么的明顯,但他沒有想到要防范。
在他的系統中,文件上傳的時候是以數據庫表中的ID命名的。上傳了一個PDF文件可能叫"13424.pdf",又上傳了一個DOC文件,就被名為"13425.doc",諸如此類。Erica雖然已經使文件不可見,但他沒考慮到文件可以直接被瀏覽器下載。用戶可以輕易的通過猜測自增ID下載到敏感的文件。
這小故事略長,但只要你努力你也能講出這么長的故事。我曾經告訴一個婦女我是Kevin Costner,他就信了,因為我也信。
身份驗證
控制敏感數據的第一步就是身份驗證,你要確保每個用戶并不是只靠自己說說就能證明他是誰。
這里我們會闡述一下上一章中密碼驗證的代碼實例,然后會講下用戶的有效登錄過程。
pass) === TRUE) { $valid = TRUE; } //其他操作等... if ($valid === TRUE) { //成功登錄 //存儲session Session::put('user', $user->id); return TRUE; } //登錄失敗 Session::put('user', NULL); return FALSE; }}
登錄后復制
我們記下了用戶的成功登錄態,只存儲了他的用戶ID。但他的登錄并不是說他可以在你的應用里為所欲為。
權限控制
除了判定用戶是否登錄有效,我們還要判定他是否有某些頁面/部分/功能的訪問權限。
我們從基礎說起。"麻瓜"和"管理員"。麻瓜是擁有少量訪問權限的普通用戶。管理員就是可以訪問所有地方的人物。管理員可以增刪用戶,管理文章等。非常重要的是普通用戶 - 麻瓜 - 不能操作這些功能
不同的框架在驗證使用權的時機各不相同。在Laravel中,Symfony是首選。在大多數系統中你可以只簡單的調用構造函數。下面是個例子;你可以根據情況改變實現方式。
我們先在UserModel中添加一個獲取用戶信息的方法
<?phpclass UserModel{//... 其他方法//... 登錄方法 function current() { $user = FALSE; if (isset($_SESSION['user']) === TRUE) { $user = DB::findById($_SESSION['user']); } return $user; }//假設還有一個獲取用戶組的方法}
登錄后復制
現在我們來驗證一下該用戶是否有訪問權。
current(); //檢查用戶是否登錄是否為管理員 if ($user === FALSE || in_array('admin', $user->groups) === FALSE) { //做好錯誤處理 die('Not Authorized'); } } //其他的管理員專用操作}
登錄后復制
你通常會在你的controllers/routes/或其他地方抽象出一個控制層。這一層會梳理你的路由恰當的擁有視圖的訪問級別。比如,/admin/*只能是"admin"用戶組訪問。POST和PUT請求只能"編輯"使用。DELETE請求只能管理員使用。
關鍵是,每個頁面都應該檢查用戶的訪問級別并確定他們是否被授權請求各種方法。你不僅需要去設想一個用戶可以看到一個表單并能POST到正確的地址,并授權執行了動作。我們還應該防止惡意用戶提交惡意數據改變你的數據庫,他在一開始就不應該看到那個頁面。
別在安全的身份驗證上掉以輕心,認真起來!你會為你自己和其他開發者節省不必要的開發時間也減少了未來的頭疼。
驗證重定向
在你的應用流程中,經常會通過表單發送一個POST請求到你的控制器,之后驗證數據,執行動作,然后重定向到應用的下一步頁面。
如果你重定向的地址包含敏感數據,用戶其實可以通過簡單的發送一個請求跳過你的期望流程直接到最后的頁面,并跳過了你的驗證步驟。
當然有很多辦法來控制啦。第一種辦法就是不再使用重定向。大多數時候當你想要用重定向的時候,你可以換成直接調用下個方法,比如把下面的替換一下:
<?phpif ($valid === TRUE && $dataSaved === TRUE) { URL::redirect('/blog/1/edit');}
登錄后復制
改成直接調用edit方法而不是使用重定向:
_edit(1);}
登錄后復制
在這個例子中,_edit()方法應該是一個保護或私有方法,這樣就不能跳過前面的步驟而用URL直接進入了。
如果實際上你需要一個其他的地址,而且你有很多入口都會指向這個目的地,那么你就必須驗證那些必須的步驟已經被執行過。
使用一個有過期時間的session變量(一般叫做flash)通常是不錯的辦法。
<?php if ($valid === TRUE && $dataSaved === TRUE) { Session::flash('blogEditValid', TRUE); URL::redirect('/blog/1/edit'); }
登錄后復制
在你的edit函數處, 你應該驗證剛剛的參數
<?php public function edit($id){ if (Session::get('blogEditValid') !== TRUE) { //錯誤信息等 die('Not Authorized'); } }
登錄后復制
模糊處理
你有沒有聽說過"security through obscurity(隱式安全)”的術語?雖然不是所有的地方都有效,但有時候真的好用。大多數應用的數據庫都會在每個表中存一個ID的字段來做主鍵。這個ID后面被用在系統的各個地方。比如URL,表單,還有API用來表示我們需要的數據。
有時候,試想,你并不想對你的用戶展示真正的數據表ID。也許你在做一個新產品的時候也不希望用戶知道他原來才剛剛是第十三個用戶。也許你的數據是公開的但你并不想被機器人蠕蟲輕易的爬去。
這時候,你可以混淆ID到非自增的字符串但可以翻譯為你的ID。如果你是這樣做的:
<?php Route::get('/edit/{id}', function($id){ //id = 432
總結
以上是生活随笔為你收集整理的构建安全的PHP应用(技术安全资讯)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 常见的打印机类型有哪几种(笔记本无线网卡
- 下一篇: Win11 2022终极更新:又把AMD