編寫安全PHP應用程式的七個習慣

thinkyoung發表於2015-04-13

編寫安全 PHP 應用程式的七個習慣

 

在提及安全性問題時,需要注意,除了實際的平臺和作業系統安全性問題之外,您還需要確保編寫安全的應用程式。在編寫 PHP 應用程式時,請應用下面的七個習慣以確保應用程式具有最好的安全性:

  • 驗證輸入
  • 保護檔案系統
  • 保護資料庫
  • 保護會話資料
  • 保護跨站點指令碼(Cross-site scripting,XSS)漏洞
  • 檢驗表單 post
  • 針對跨站點請求偽造(Cross-Site Request Forgeries,CSRF)進行保護

驗證輸入

在提及安全性問題時,驗證資料是您可能採用的最重要的習慣。而在提及輸入時,十分簡單:不要相信使用者。您的使用者可能十分優秀,並且大多數使用者可能完全按照期望來使用應用程式。但是,只要提供了輸入的機會,也就極有可能存在非常糟糕的輸入。作為一名應用程式開發人員,您必須阻止應用程式接受錯誤的輸入。仔細考慮使用者輸入的位置及正確值將使您可以構建一個健壯、安全的應用程式。

雖然後文將介紹檔案系統與資料庫互動,但是下面列出了適用於各種驗證的一般驗證提示:

  • 使用白名單中的值
  • 始終重新驗證有限的選項
  • 使用內建轉義函式
  • 驗證正確的資料型別(如數字)

白名單中的值(White-listed value)是正確的值,與無效的黑名單值(Black-listed value)相對。兩者之間的區別是,通常在進行驗證時,可能值的列表或範圍小於無效值的列表或範圍,其中許多值可能是未知值或意外值。

在進行驗證時,記住設計並驗證應用程式允許使用的值通常比防止所有未知值更容易。例如,要把欄位值限定為所有數字,需要編寫一個確保輸入全都是數字的例程。不要編寫用於搜尋非數字值並在找到非數字值時標記為無效的例程。

保護檔案系統

2000 年 7 月,一個 Web 站點洩露了儲存在 Web 伺服器的檔案中的客戶資料。該 Web 站點的一個訪問者使用 URL 檢視了包含資料的檔案。雖然檔案被放錯了位置,但是這個例子強調了針對攻擊者保護檔案系統的重要性。

如果 PHP 應用程式對檔案進行了任意處理並且含有使用者可以輸入的變數資料,請仔細檢查使用者輸入以確保使用者無法對檔案系統執行任何不恰當的操作。清單 1 顯示了下載具有指定名的影像的 PHP 站點示例。

清單 1. 下載檔案

				
<?php
if ($_POST[`submit`] == `Download`) {
    $file = $_POST[`fileName`];
    header("Content-Type: application/x-octet-stream");
    header("Content-Transfer-Encoding: binary");
    header("Content-Disposition: attachment; filename="" . $file . "";" );
    $fh = fopen($file, `r`);
    while (! feof($fh))
    {
        echo(fread($fh, 1024));
    }
    fclose($fh);
} else {
    echo("<html><head><");
        echo("title>Guard your filesystem</title></head>");
    echo("<body><form id="myFrom" action="" . $_SERVER[`PHP_SELF`] .
        "" method="post">");
    echo("<div><input type="text" name="fileName" value="");
    echo(isset($_REQUEST[`fileName`]) ? $_REQUEST[`fileName`] : ``);
    echo("" />");
    echo("<input type="submit" value="Download" name="submit" /></div>");
    echo("</form></body></html>");
}

 

正如您所見,清單 1 中比較危險的指令碼將處理 Web 伺服器擁有讀取許可權的所有檔案,包括會話目錄中的檔案(請參閱 “保護會話資料”),甚至還包括一些系統檔案(例如 /etc/passwd)。為了進行演示,這個示例使用了一個可供使用者鍵入檔名的文字框,但是可以在查詢字串中輕鬆地提供檔名。

同時配置使用者輸入和檔案系統訪問權十分危險,因此最好把應用程式設計為使用資料庫和隱藏生成的檔名來避免同時配置。但是,這樣做並不總是有效。清單 2 提供了驗證檔名的示例例程。它將使用正規表示式以確保檔名中僅使用有效字元,並且特別檢查圓點字元:..

清單 2. 檢查有效的檔名字元

				
function isValidFileName($file) {
    /* don`t allow .. and allow any "word" character  / */
    return preg_match(`/^(((?:.)(?!.))|w)+$/`, $file);
}

 

 

回頁首

保護資料庫

2008 年 4 月,美國某個州的獄政局在查詢字串中使用了 SQL 列名,因此洩露了保密資料。這次洩露允許惡意使用者選擇需要顯示的列、提交頁面並獲得資料。這次洩露顯示了使用者如何能夠以應用程式開發人員無法預料的方法執行輸入,並表明了防禦 SQL 注入攻擊的必要性。

清單 3 顯示了執行 SQL 語句的示例指令碼。在本例中,SQL 語句是允許相同攻擊的動態語句。此表單的所有者可能認為表單是安全的,因為他們已經把列名限定為選擇列表。但是,程式碼疏忽了關於表單欺騙的最後一個習慣 — 程式碼將選項限定為下拉框並不意味著其他人不能夠釋出含有所需內容的表單(包括星號 [*])。

清單 3. 執行 SQL 語句

				
<html>
<head>
<title>SQL Injection Example</title>
</head>
<body>
<form id="myFrom" action="<?php echo $_SERVER[`PHP_SELF`]; ?>"
    method="post">
<div><input type="text" name="account_number"
    value="<?php echo(isset($_POST[`account_number`]) ? 
        $_POST[`account_number`] : ``); ?>" />
<select name="col">
<option value="account_number">Account Number</option>
<option value="name">Name</option>
<option value="address">Address</option>
</select>
<input type="submit" value="Save" name="submit" /></div>
</form>
<?php
if ($_POST[`submit`] == `Save`) {
    /* do the form processing */
    $link = mysql_connect(`hostname`, `user`, `password`) or 
        die (`Could not connect` . mysql_error());
    mysql_select_db(`test`, $link);
		
		$col = $_POST[`col`];

    $select = "SELECT " . $col . " FROM account_data WHERE account_number = " 
        . $_POST[`account_number`] . ";" ;
    echo `<p>` . $select . `</p>`;

    $result = mysql_query($select) or die(`<p>` . mysql_error() . `</p>`);

    echo `<table>`;
    while ($row = mysql_fetch_assoc($result)) {
        echo `<tr>`;
        echo `<td>` . $row[$col] . `</td>`;
        echo `</tr>`;
    }
    echo `</table>`;

    mysql_close($link);
}
?>
</body>
</html>

 

因此,要形成保護資料庫的習慣,請儘可能避免使用動態 SQL 程式碼。如果無法避免動態 SQL 程式碼,請不要對列直接使用輸入。清單 4 顯示了除使用靜態列外,還可以向帳戶編號欄位新增簡單驗證例程以確保輸入值不是非數字值。

清單 4. 通過驗證和 mysql_real_escape_string() 提供保護

				
<html>
<head>
<title>SQL Injection Example</title>
</head>
<body>
<form id="myFrom" action="<?php echo $_SERVER[`PHP_SELF`]; ?>"
    method="post">
<div><input type="text" name="account_number"
    value="<?php echo(isset($_POST[`account_number`]) ? 
        $_POST[`account_number`] : ``); ?>" /> <input type="submit"
    value="Save" name="submit" /></div>
</form>
<?php
function isValidAccountNumber($number) 
{
    return is_numeric($number);
}

if ($_POST[`submit`] == `Save`) {

    /* Remember habit #1--validate your data! */
    if (isset($_POST[`account_number`]) &&
    isValidAccountNumber($_POST[`account_number`])) {

        /* do the form processing */
        $link = mysql_connect(`hostname`, `user`, `password`) or
        die (`Could not connect` . mysql_error());
        mysql_select_db(`test`, $link);

        $select = sprintf("SELECT account_number, name, address " .
		" FROM account_data WHERE account_number = %s;",
        mysql_real_escape_string($_POST[`account_number`]));
        echo `<p>` . $select . `</p>`;

        $result = mysql_query($select) or die(`<p>` . mysql_error() . `</p>`);

        echo `<table>`;
        while ($row = mysql_fetch_assoc($result)) {
            echo `<tr>`;
            echo `<td>` . $row[`account_number`] . `</td>`;
            echo `<td>` . $row[`name`] . `</td>`;
            echo `<td>` . $row[`address`] . `</td>`;
            echo `</tr>`;
        }
        echo `</table>`;

        mysql_close($link);
    } else {
        echo "<span style="font-color:red">" .
    "Please supply a valid account number!</span>";

    }
}
?>
</body>
</html>

 

本例還展示了 mysql_real_escape_string() 函式的用法。此函式將正確地過濾您的輸入,因此它不包括無效字元。如果您一直依賴於 magic_quotes_gpc,那麼需要注意它已被棄用並且將在 PHP V6 中刪除。從現在開始應避免使用它並在此情況下編寫安全的 PHP 應用程式。此外,如果使用的是 ISP,則有可能您的 ISP 沒有啟用 magic_quotes_gpc

最後,在改進的示例中,您可以看到該 SQL 語句和輸出沒有包括動態列選項。使用這種方法,如果把列新增到稍後含有不同資訊的表中,則可以輸出這些列。如果要使用框架以與資料庫結合使用,則您的框架可能已經為您執行了 SQL 驗證。確保查閱文件以保證框架的安全性;如果仍然不確定,請進行驗證以確保穩妥。即使使用框架進行資料庫互動,仍然需要執行其他驗證。

 

回頁首

保護會話

預設情況下,PHP 中的會話資訊將被寫入臨時目錄。考慮清單 5 中的表單,該表單將顯示如何儲存會話中的使用者 ID 和帳戶編號。

清單 5. 儲存會話中的資料

				
<?php
session_start();
?>
<html>
<head>
<title>Storing session information</title>
</head>
<body>
<?php
if ($_POST[`submit`] == `Save`) {
    $_SESSION[`userName`] = $_POST[`userName`];
    $_SESSION[`accountNumber`] = $_POST[`accountNumber`];
}
?>
<form id="myFrom" action="<?php echo $_SERVER[`PHP_SELF`]; ?>"
    method="post">
<div><input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="text" name="userName"
    value="<?php echo(isset($_POST[`userName`]) ? $_POST[`userName`] : ``); ?>" />
<br />
<input type="text" name="accountNumber"
    value="<?php echo(isset($_POST[`accountNumber`]) ? 
    $_POST[`accountNumber`] : ``); ?>" />
<br />
<input type="submit" value="Save" name="submit" /></div>
</form>
</body>
</html>

 

清單 6 顯示了 /tmp 目錄的內容。

清單 6. /tmp 目錄中的會話檔案

				
-rw-------  1 _www    wheel       97 Aug 18 20:00 sess_9e4233f2cd7cae35866cd8b61d9fa42b

 

正如您所見,在輸出時(參見清單 7),會話檔案以非常易讀的格式包含資訊。由於該檔案必須可由 Web 伺服器使用者讀寫,因此會話檔案可能為共享伺服器中的所有使用者帶來嚴重的問題。除您之外的某個人可以編寫指令碼來讀取這些檔案,因此可以嘗試從會話中取出值。

清單 7. 會話檔案的內容

				
userName|s:5:"ngood";accountNumber|s:9:"123456789";

 

儲存密碼

不管是在資料庫、會話、檔案系統中,還是在任何其他表單中,無論如何密碼都決不能儲存為純文字。處理密碼的最佳方法是將其加密儲存並相互比較加密的密碼。雖然如此,在實踐中人們仍然把密碼儲存到純文字中。只要使用可以傳送密碼而非重置密碼的 Web 站點,就意味著密碼是儲存在純文字中或者可以獲得用於解密的程式碼(如果加密的話)。即使是後者,也可以找到並使用解密程式碼。

您可以採取兩項操作來保護會話資料。第一是把您放入會話中的所有內容加密。但是正因為加密資料並不意味著絕對安全,因此請慎重採用這種方法作為保護會話的惟一方式。備選方法是把會話資料儲存在其他位置中,比方說資料庫。您仍然必須確保鎖定資料庫,但是這種方法將解決兩個問題:第一,它將把資料放到比共享檔案系統更加安全的位置;第二,它將使您的應用程式可以更輕鬆地跨越多個 Web 伺服器,同時共享會話可以跨越多個主機。

要實現自己的會話永續性,請參閱 PHP 中的session_set_save_handler() 函式。使用它,您可以將會話資訊儲存在資料庫中,也可以實現一個用於加密和解密所有資料的處理程式。清單 8 提供了實現的函式用法和函式骨架示例。您還可以在 參考資料 小節中檢視如何使用資料庫。

清單 8. session_set_save_handler() 函式示例

				
function open($save_path, $session_name)
{
    /* custom code */
    return (true);
}

function close()
{
    /* custom code */
    return (true);
}

function read($id)
{
    /* custom code */
    return (true);
}

function write($id, $sess_data)
{
    /* custom code */
    return (true);
}

function destroy($id)
{
    /* custom code */
    return (true);
}

function gc($maxlifetime)
{
    /* custom code */
    return (true);
}

session_set_save_handler("open", "close", "read", "write", "destroy", "gc");

 

 

回頁首

針對 XSS 漏洞進行保護

XSS 漏洞代表 2007 年所有歸檔的 Web 站點的大部分漏洞(請參閱 參考資料)。當使用者能夠把 HTML 程式碼注入到您的 Web 頁面中時,就是出現了 XSS 漏洞。HTML 程式碼可以在指令碼標記中攜帶 JavaScript 程式碼,因而只要提取頁面就允許執行 JavaScript。清單 9 中的表單可以表示論壇、維基、社會網路或任何可以輸入文字的其他站點。

清單 9. 輸入文字的表單

				
<html>
<head>
<title>Your chance to input XSS</title>
</head>
<body>
<form id="myFrom" action="showResults.php" method="post">
<div><textarea name="myText" rows="4" cols="30"></textarea><br />
<input type="submit" value="Delete" name="submit" /></div>
</form>
</body>
</html>

 

清單 10 演示了允許 XSS 攻擊的表單如何輸出結果。

清單 10. showResults.php

				
<html>
<head>
<title>Results demonstrating XSS</title>
</head>
<body>
<?php
echo("<p>You typed this:</p>");
echo("<p>");
echo($_POST[`myText`]);
echo("</p>");
?>
</body>
</html>

 

清單 11 提供了一個基本示例,在該示例中將彈出一個新視窗並開啟 Google 的主頁。如果您的 Web 應用程式不針對 XSS 攻擊進行保護,則會造成嚴重的破壞。例如,某個人可以新增模仿站點樣式的連結以達到欺騙(phishing)目的(請參閱 參考資料)。

清單 11. 惡意輸入文字樣例

				
<script type="text/javascript">myRef = window.open(`http://www.google.com`,`mywin`,
`left=20,top=20,width=500,height=500,toolbar=1,resizable=0`);</script>

 

要防止受到 XSS 攻擊,只要變數的值將被列印到輸出中,就需要通過 htmlentities() 函式過濾輸入。記住要遵循第一個習慣:在 Web 應用程式的名稱、電子郵件地址、電話號碼和帳單資訊的輸入中用白名單中的值驗證輸入資料。

下面顯示了更安全的顯示文字輸入的頁面。

清單 12. 更安全的表單

				
<html>
<head>
<title>Results demonstrating XSS</title>
</head>
<body>
<?php
echo("<p>You typed this:</p>");
echo("<p>");
echo(htmlentities($_POST[`myText`]));
echo("</p>");
?>
</body>
</html>

 

 

回頁首

針對無效 post 進行保護

表單欺騙 是指有人把 post 從某個不恰當的位置發到您的表單中。欺騙表單的最簡單方法就是建立一個通過提交至表單來傳遞所有值的 Web 頁面。由於 Web 應用程式是沒有狀態的,因此沒有一種絕對可行的方法可以確保所釋出資料來自指定位置。從 IP 地址到主機名,所有內容都是可以欺騙的。清單 13 顯示了允許輸入資訊的典型表單。

清單 13. 處理文字的表單

				
<html>
<head>
<title>Form spoofing example</title>
</head>
<body>
<?php
if ($_POST[`submit`] == `Save`) {
    echo("<p>I am processing your text: ");
    echo($_POST[`myText`]);
    echo("</p>");
}
?>
</body>
</html>

 

清單 14 顯示了將釋出到清單 13 所示表單中的表單。要嘗試此操作,您可以把該表單放到 Web 站點中,然後把清單 14 中的程式碼另存為桌面上的 HTML 文件。在儲存表單後,在瀏覽器中開啟該表單。然後可以填寫資料並提交表單,從而觀察如何處理資料。

清單 14. 收集資料的表單

				
<html>
<head>
<title>Collecting your data</title>
</head>
<body>
<form action="processStuff.php" method="post">
<select name="answer">
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
<input type="submit" value="Save" name="submit" />
</form>
</body>
</html>

 

表單欺騙的潛在影響是,如果擁有含下拉框、單選按鈕、核取方塊或其他限制輸入的表單,則當表單被欺騙時這些限制沒有任何意義。考慮清單 15 中的程式碼,其中包含帶有無效資料的表單。

清單 15. 帶有無效資料的表單

				
<html>
<head>
<title>Collecting your data</title>
</head>
<body>
<form action="http://path.example.com/processStuff.php" 
    method="post"><input type="text" name="answer"
    value="There is no way this is a valid response to a yes/no answer..." />
<input type="submit" value="Save" name="submit" />
</form>
</body>
</html>

 

思考一下:如果擁有限制使用者輸入量的下拉框或單選按鈕,您可能會認為不用擔心驗證輸入的問題。畢竟,輸入表單將確保使用者只能輸入某些資料,對吧?要限制表單欺騙,需要進行驗證以確保釋出者的身份是真實的。您可以使用一種一次性使用標記,雖然這種技術仍然不能確保表單絕對安全,但是會使表單欺騙更加困難。由於在每次呼叫表單時都會更改標記,因此想要成為攻擊者就必須獲得傳送表單的例項,去掉標記,並把它放到假表單中。使用這項技術可以阻止惡意使用者構建持久的 Web 表單來嚮應用程式釋出不適當的請求。清單 16 提供了一種表單標記示例。

清單 16. 使用一次性表單標記

				
<?php
session_start();
?>
<html>
<head>
<title>SQL Injection Test</title>
</head>
<body>
<?php

echo `Session token=` . $_SESSION[`token`];
echo `<br />`;
echo `Token from form=` . $_POST[`token`];
echo `<br />`;

if ($_SESSION[`token`] == $_POST[`token`]) {
    /* cool, it`s all good... create another one */

} else {
    echo `<h1>Go away!</h1>`;
}
$token = md5(uniqid(rand(), true)); 
$_SESSION[`token`] = $token; 
?>
<form id="myFrom" action="<?php echo $_SERVER[`PHP_SELF`]; ?>"
    method="post">
<div><input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="text" name="myText"
    value="<?php echo(isset($_POST[`myText`]) ? $_POST[`myText`] : ``); ?>" />
<input type="submit" value="Save" name="submit" /></div>
</form>
</body>
</html>

 

 

回頁首

針對 CSRF 進行保護

跨站點請求偽造(CSRF 攻擊)是利用使用者許可權執行攻擊的結果。在 CSRF 攻擊中,您的使用者可以輕易地成為預料不到的幫凶。清單 17 提供了執行特定操作的頁面示例。此頁面將從 cookie 中查詢使用者登入資訊。只要 cookie 有效,Web 頁面就會處理請求。

清單 17. CSRF 示例

				
<img src="http://www.example.com/processSomething?id=123456789" />

 

CSRF 攻擊通常是以 <img> 標記的形式出現的,因為瀏覽器將在不知情的情況下呼叫該 URL 以獲得影像。但是,影像來源可以是根據傳入引數進行處理的同一個站點中的頁面 URL。當此 <img> 標記與 XSS 攻擊結合在一起時 — 在已歸檔的攻擊中最常見 — 使用者可以在不知情的情況下輕鬆地對其憑證執行一些操作 — 因此是偽造的。

為了保護您免受 CSRF 攻擊,需要使用在檢驗表單 post 時使用的一次性標記方法。此外,使用顯式的 $_POST 變數而非 $_REQUEST。清單 18 演示了處理相同 Web 頁面的糟糕示例 — 無論是通過 GET 請求呼叫頁面還是通過把表單釋出到頁面中。

清單 18. 從 $_REQUEST 中獲得資料

				
<html>
<head>
<title>Processes both posts AND gets</title>
</head>
<body>
<?php
if ($_REQUEST[`submit`] == `Save`) {
    echo("<p>I am processing your text: ");
    echo(htmlentities($_REQUEST[`text`]));
    echo("</p>");
}
?>
</body>
</html>

 

清單 19 顯示了只使用表單 POST 的乾淨頁面。

清單 19. 僅從 $_POST 中獲得資料

				
<html>
<head>
<title>Processes both posts AND gets</title>
</head>
<body>
<?php
if ($_POST[`submit`] == `Save`) {
    echo("<p>I am processing your text: ");
    echo(htmlentities($_POST[`text`]));
    echo("</p>");
}
?>
</body>
</html>

 

 

回頁首

結束語

從這七個習慣開始嘗試編寫更安全的 PHP Web 應用程式,可以幫助您避免成為惡意攻擊的受害者。和許多其他習慣一樣,這些習慣最開始可能很難適應,但是隨著時間的推移遵循這些習慣會變得越來越自然。

記住第一個習慣是關鍵:驗證輸入。在確保輸入不包括無效值之後,可以繼續保護檔案系統、資料庫和會話。最後,確保 PHP 程式碼可以抵抗 XSS 攻擊、表單欺騙和 CSRF 攻擊。形成這些習慣後可以幫助您抵禦一些簡單的攻擊。


相關文章