2011年7月6日 星期三

PHP安全程式寫作範例

PHP安全程式寫作範例

  • 對象: 程式設計人員
  • 難易度: ★★★☆☆
  • 技術新舊: ★★☆☆☆
  • 閱讀所需時間: 2 小時

PHP安全程式寫作

網路應用程式的安全性如今已是大家重視的話題,但是在程式開發過程,常常是到了最後一刻才將security的部分補足。
其中有一個原因是安全程式碼撰寫的概念並未建立,如果在程式撰寫階段就將安全性的問題考慮進去,在日後檢驗與維護系統安全時,將會更容易。

因此,我們針對從客戶端送進來的資料常見的六種攻擊方式,分別提示PHP程式如何撰寫來防禦這些攻擊行為:
1. SQL 注入攻擊 (SQL injection attack)
2. 操縱 GET 的值 (manipulating GET variables)
3. 緩衝器溢位攻擊 (buffer overflow attack)
4. 跨站腳本攻擊 (cross-site scripting)
5. 操縱瀏覽器內的資料 (manipulating data inside the browser)
6. 遠程表格遞交 (remote form posting)

另外,下面有幾項簡單的安全性原則是每一個PHP程式撰寫者都應該遵守的:
1.永遠不信任外來的資料
外來的資料包括所有不是直接由programmer撰寫於PHP code裡的資料,像是從GET、POST取得的input、database、configuration file、session variable、cookie等,這些資料在經過驗證或消毒以前都必須將它視為tainted。而通常最簡單的消毒input方式是利用regular expression,定義你所接受的字串類型或內容。

2.確保一些PHP設定例如register_globals、display_errors等已關閉
如果開啟register_globals,將造成GET和POST的值($_GET、$_POST)都能直接使用$variable去接收,這將造成安全性問題。另外,當系統上線後錯誤報告必須謹慎處理,不該讓它出現在瀏覽畫面上,許多attacker就是利用錯誤報告內的資訊來猜想你系統的相關內容,所以要記得關閉display_errors等error-reporting的功能。

3.寫簡潔清晰的程式碼,使安全漏洞更容易被檢查出來
避免使用艱深的語法,這種寫法或許會有較好的效能,但是如果無法簡單地讓人了解code的內容,也會造成安全性判斷的困難。

4.把「深度防禦」列為座右銘
對於網路應用程式的安全必須為全面性的,當你已利用regex檢驗GET的input值後,當然也不能遺忘SQL query內是否也有安全性的疑慮。

SQL注入攻擊(SQL injection attack)

現今的網站應用程式架構常由$_GET, $_POST取得使用者輸入的資料,並經由 database query 查詢資料庫的資料以動態地產生網頁,但若是網站應用程式沒有仔細驗證使用者的輸入值,則惡意的使用者便可以輸入惡意的資料當作查詢一部分給資料庫執行,而造成程式設計師預期外的結果。
例如下面這個例子,我們的資料庫裡有username和password兩個欄位,我們建立一個登入頁面,讓使用者輸入帳號和密碼以登入網站;當使用者輸入完資料送出後,表單會用post方式將使用者的資料傳給verify.php去做身份驗證。

登入表單:
<form action="verify.php" method="post">
<p>Username:<input type='text' name='user'/></p>
<p>Password:<input type='password' name='pw'/></p>
<p><input type='submit' value='login'/></p>
</form>
verify.php
$okay = 0;
$username = $_POST['user'];
$pw = $_POST['pw'];
$sql = "select count(*) as ctr from users where username='".$username."' and password='". $pw."' limit 1";
$result = mysql_query($sql);
while ($data = mysql_fetch_object($result)){
if ($data->ctr == 1){
//they're okay to enter the application!
$okay = 1;
}
}
if ($okay){
$_SESSION['loginokay'] = true;
header("index.php");
}else{
header("login.php");
}
我們仔細檢視verify.php的程式碼,將發現安全上的漏洞,可能造成SQL injection attack。如果有一個惡意的使用者,在username的部分輸入foo,而在password部分輸入' or '1'='1,這將會造成整個SQL query 呈現以下的形式,由於使用者所加上的惡意輸入使得WHERE中整個判斷式為真,所以此使用者即便沒有帳號密碼也順利登入系統。
$sql = "select count(*) as ctr from users where
username='foo' and password='' or '1'='1' limit 1";
這種由於程式碼寫作疏忽所造成SQL injection的安全漏洞,可以簡單地用PHP內建的函式mysql_real_escape_string()來解決,mysql_real_escape_string()是用來脫逸資料庫語法中的特殊字元,像是\ ,',”。
因此我們將原本的verify.php程式加上mysql_real_escape_string()重新撰寫如下:
$okay = 0;
$username = $_POST['user'];
$pw = $_POST['pw'];
$sql = "select count(*) as ctr from users where 
username='".mysql_real_escape_string($username)."'
and password='". mysql_real_escape_string($pw)."' limit 1";
$result = mysql_query($sql);
while ($data = mysql_fetch_object($result)){
if ($data->ctr == 1){
//they're okay to enter the application!
$okay = 1;
}
}
if ($okay){
$_SESSION['loginokay'] = true;
header("index.php");
}else{
header("login.php");
}
則當username的部分輸入foo,而password部分輸入' or '1'='1時,會產生以下的SQL query,因為 ' (單引號)已被脫逸為string的一部分,所以此查詢會正常執行,去資料庫找尋是否有符合的帳號密碼配對。
$sql = "select count(*) as ctr from users where \
username='foo' and password='\' or \'1\'=\'1' limit 1"

操縱GET的值(manipulating GET variables)

網頁應用程式的某些撰寫方式可能會讓使用者看到與網站內部程式或檔案相關的資訊,例如一個網址 template.php?pid=321,在URL中?之後接的為 GET query string,則該網頁可用 $_GET['pid'] 去接收此變數的值。例如下面的例子,利用 $_GET['pid'] 的值來動態產生頁面:
$pid = $_GET['pid'];
//we create an object of a fictional
class Page
$obj = new Page;
$content = $obj->fetchPage($pid);
//and now we have a bunch of PHP that displays the page
這樣的程式撰寫方式可能會有安全上的漏洞,因為我們不能確定GET變數的值為安全的,有可能是使用者手動輸入的不安全 SQL command 或長度很長的字串。
但我們可以確定的是 pid 的值必須是一個數字,因此我們可以利用 PHP 內建的函式 is_numeric() 去檢查 $_GET['pid'] 的值,不過 is_numeric() 函式仍有一些不足之處;因此最好的方式還是利用 regular expression 來確保變數值為數字。我們可以將程式改寫為:
$pid = $_GET['pid'];
 
if (strlen($pid)){
if (!ereg("^[0-9]+$",$pid)){
//do something appropriate, like maybe logging them out or sending them back to home page
}
}else{
//empty $pid, so send them back to the home page
}
$obj = new Page;
$content = $obj->fetchPage($pid);
//and now we have a bunch of PHP that displays the page
我們先用 strlen() 去看 $_GET['pid'] 是否有長度,如果有就用 all-number regular expression 檢查變數內容是否為數字型態,因此如果變數內容包含 letters, slashes, dots, or hexadecimal-like notations 都會被過濾掉而無法執行此頁面,防止可能的攻擊產生。
那如果使用者試圖輸入很長的數字串來造成 buffer overflow attack 呢?(下一個部分會對 buffer overflow attack 做更詳細的介紹)我們可以藉由 strlen() 函式來檢查字串長度。
$pid = $_GET['pid'];
 
if (strlen($pid)){
if (!ereg("^[0-9]+$",$pid) && strlen($pid) > 5){
//do something appropriate, like maybe logging them out or sending them back to home page
}
}else{
//empty $pid, so send them back to the home page
}
$obj = new Page;
$content = $obj->fetchPage($pid);
//and now we have a bunch of PHP that displays the page

緩衝器溢位攻擊(buffer overflow attack)

buffer overflow attack 主要是藉由 overflow the memory allocation buffer,造成網頁應用程式 denial of service、資料損毀或藉機對主機執行惡意的程式。要防止 buffer overflow attack 最大的重點就是檢查使用者輸入資料的長度。例如下面的表單設定 input text 的 maxlength 為40,並且在後端有 substr() 做第二層防範。
<?php
if ($_POST['submit'] == "go"){
$name = substr($_POST['name'],0,40);
//continue processing....
}
?>
<form action="<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<p><input type="submit" name="submit" value="go"/></p>
</form>
同時使用 maxlength 參數值及 substr() 實現了縱深防禦的概念,利用 maxlength 可以直接防止使用者對資料庫輸入過長的字串;後端的 substr() 確保程式執行中變數如果被操作,長度過長的字串仍會被截斷,不會對應用程式造成傷害。
前一個部分(操縱GET的值中)提到的例子是採用 strlen() 來檢查 $_GET['pid'] 字串的長度,我們也可以藉由 substr() 取得符合我們所需長度的字串後再執行之後的程式碼。
$pid = $_GET['pid'];
if (strlen($pid)){
if (!ereg("^[0-9]+$",$pid)){
//if non numeric $pid, send them back to home page
}
}else{
//empty $pid, so send them back to the home page
}
//we have a numeric pid, but it may be too long, so let's check
if (strlen($pid)>5){
$pid = substr($pid,0,5);
}
$obj = new Page;
$content = $obj->fetchPage($pid);
//and now we have a bunch of PHP that displays the page
此外,不只是過長的 numbers or letters 會造成 buffer overflow attack,過長的 hexadecimal characters (如:\xA3 or \xFF)也會造成同樣效果。不過防止此類攻擊的概念是一樣的:就是確保字串長度沒有超過指定的長度;另外可以配合 regular expression 去除 hexadecimal 的字串。
<?php
if ($_POST['submit'] == "go"){
$name = substr($_POST['name'],0,40);
//clean out any potential hexadecimal characters
$name = cleanHex($name);
//continue processing....
}
function cleanHex($input){
$clean = preg_replace("![\][xX]([A-Fa-f0-9]{1,3})!", "",$input);
return $clean;
}
?>
<form action="<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<p><input type="submit" name="submit" value="go"/></p>
</form>

跨站腳本攻擊(cross-site scripting)

cross-site scripting (XSS) 通常牽扯到使用者可以輸入資料的表單或任何供使用者 input 的地方。例如一個 guestbook 能讓使用者輸入 names, e-mail address, message,惡意的使用者可藉此輸入一些 JavaScript 讓瀏覽者 redirect 到另一個網站或偷取 cookie。
這種 XSS 攻擊可利用 PHP 內建的函式 strip_tags() 去除字串中的 HTMLPHP 標籤,同時這函式也可以自己設定可接受的 tags(例如:<b>、<i>)。
<?php
if ($_POST['submit'] == "go"){
//strip_tags
$name = strip_tags($_POST['name']);
$name = substr($name,0,40);
}
?>
<form action="<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<p><input type="submit" name="submit" value="go"/></p>
</form>
但相對地,如果你開放使用者使用 HTML 標籤來發表內容的話,就必須使用 htmlspecialchars() 讓使用者輸入的 HTML 標籤保留它的意義,例如此函式會將 ampersand (&) 轉化成 &amp ;將 < 和 > 視為 HTML entities。

操縱瀏覽器內的資料(manipulating data inside the browser)

有些瀏覽器的 plug-in 提供使用者操作 header 或 form 資料內容的功能。例如 firefox plug-in:Tamper Data 可以輕鬆修改 HTTP headers、cookies、hidden text fields 的參數……等。因此在使用者點選 Submit 傳送出表單前,他可以先開啟 Tamper Data 將資料做些修改後再傳送。
以下面這個程式為例,我們在表單中加了許多 hidden text fields,其中像 action field 的值為 create,如果程式執行中會使用此值做 SQL command;那惡意的使用者就可藉由 plug-in 改變 hidden text fields 的內容,例如改為 delete 造成資料庫傷害。
<?php
if ($_POST['submit'] == "go"){
//strip_tags
$name = strip_tags($_POST['name']);
$name = substr($name,0,40);
//clean out any potential hexadecimal characters
$name = cleanHex($name);
//continue processing....
}
function cleanHex($input){
$clean = preg_replace("![\][xX]([A-Fa-f0-9]{1,3})!", "",$input);
return $clean;
}
?>
<form action="<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<input type="hidden" name="table" value="users"/>
<input type="hidden" name="action" value="create"/>
<input type="hidden" name="status" value="live"/>
<p><input type="submit" name="submit" value="go"/></p>
</form>
要防範這類的攻擊,我們只能假設使用者可能擁有類似 Tamper Data 功能的工具,因此在撰寫程式碼時要小心謹慎,避免透露過多和系統操作相關的訊息,盡量不要使用 hidden 的方式傳送變數……等。

遠程表格遞交(remote form posting)

使用者瀏覽網頁時,任何人都可以用「另存新檔」的方式複製一份網頁檔案到自己電腦中;而惡意的使用者可複製一份含有表單的網頁,將 action 的值改為正確的指向,並且修改任何內容後再傳送,這時對方的 server 會把它視為合法的資料,但事實上,這個 request 並非由本身 server 所發出。
要防制 remote form posting 的攻擊可產生一個 token,為一個獨特的字串或者是一個 timestamp,再將這個 token 存進 session 以及放入表單中,藉由檢查這兩個 tokens 是否相同,來判定是否是 remote 的表單發出的 request。因為 session 的值會存放在 server 中,並且不會轉移。
以下的程式產生 random token,使用到 PHP 內建函式 md5()、uniqid()、rand()。
<?php
session_start();
if ($_POST['submit'] == "go"){
//check token
if ($_POST['token'] == $_SESSION['token']){
//strip_tags
$name = strip_tags($_POST['name']);
$name = substr($name,0,40);
//clean out any potential hexadecimal characters
$name = cleanHex($name);
//continue processing....
}else{
//stop all processing! remote form posting attempt!
}
}
$token = md5(uniqid(rand(), true));
$_SESSION['token']= $token;
function cleanHex($input){
$clean = preg_replace("![\][xX]([A-Fa-f0-9]{1,3})!", "",$input);
return $clean;
}
?>
<form action="<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<input type="hidden" name="token" value="<?php echo $token;?>"/>
<p><input type="submit" name="submit" value="go"/></p>
</form>

沒有留言:

張貼留言