原著:Kevin Yank 轉自:www.linuxforum.net (恭喜再此開通)
在很長一段時間內,PHP作為伺服器端指令碼語言的最大賣點之一就是會為從表單提交的值自動建立一個全域變數。在PHP 4.1中,PHP的製作者們推薦了一個訪問提交資料的替代手段。在PHP 4.2中,他們取消了那種老的做法!正如我將在這篇文章中解釋的那樣,作出這樣的變化的目的是出於安全性的考慮。我們將研究PHP在處理表單提交及其它資料時的新的做法,並說明為什麼這樣做會提高代碼的安全性。
這裡有什麼錯誤?
看看下面的這段PHP指令碼,它用來在輸入的使用者名稱及口令正確時授權訪問一個Web頁面:
<?php
// 檢查使用者名稱及口令
if ($username == 'kevin' and $password == 'secret')
$authorized = true;
?>
<?php if (!$authorized): ?>
<!-- 未授權的使用者將在這裡給予提示 -->
<p>Please enter your username and password:</p>
<form action="<?=$PHP_SELF?>" method="POST">
<p>Username: <input type="text" name="username" /><br />
Password: <input type="password" name="password" /><br />
<input type="submit" /></p>
</form>
<?php else: ?>
<!-- 有安全要求的HTML內容 -->
<?php endif; ?>
OK,我相信大約半數的讀者會不屑的說“太愚蠢了-- 我不會犯這樣的錯誤的!”但是我保證有很多的讀者會想“嗨,沒什麼問題啊,我也會這麼寫的!”當然還會有少數人會對這個問題感到困惑(“什麼是PHP?”)。PHP被設計為一個“好的而且容易的”指令碼語言,初學者可以在很短的時間內學會使用它;它也應該能夠避免初學者犯上面的錯誤。
再回到剛才的問題,上面的代碼中存在的問題是你可以很容易地獲得訪問的權力,而不需要提供正確的使用者名稱和口令。只在要你的瀏覽器的地址欄的最後添加?authorized=1。因為PHP會自動地為每一個提交的值建立一個變數 -- 不論是來自動一個提交的表單、URL查詢字串還是一個cookie -- 這會將$authorized設定為1,這樣一個未授權的使用者也可以突破安全限制。
那麼,怎麼簡單地解決這個問題呢?只要在程式的開頭將$authorized預設設定為false。這個問題就不存在了!$authorized是一個完全在程式碼中建立的變數;但是為什麼開發人員得為每一個惡意的使用者提交的變數擔心呢?
PHP 4.2作了什麼改變?
在PHP 4.2中,新安裝的PHP中的register_globals選項預設為關閉,因此EGPCS值(EGPCS是Environment、Get、Post、Cookies、Server的縮寫 -- 這是PHP中外部變數來源的全部範圍)不會被作為全域變數來建立。當然,這個選項還可以通過手工來開啟,但是PHP的開發人員推薦你將其關閉。要貫徹他們的意圖,你需要使用其它的方法來擷取這些值。
從PHP 4.1開始,EGPCS值就可以從一組指定的數組中獲得:
$_ENV -- 包含系統內容變數
$_GET -- 包含查詢字串中的變數,以及提交方法為GET的表單中的變數
$_POST -- 包含提交方式為POST的表單中的變數
$_COOKIE -- 包含所有cookie變數
$_SERVER -- 包含伺服器變數,例如HTTP_USER_AGENT
$_REQUEST -- 包含$_GET、$_POST和$_COOKIE的全部內容
$_SESSION -- 包含所有登入的session變數
在PHP 4.1之前,當開發人員關閉register_globals選項(這也被考慮為提高PHP效能的一種方法)後,必須使用諸如$HTTP_GET_VARS這樣的令人討厭的名字來擷取這些變數。這些新的變數名不僅僅短,而且它們還有其他優點。
首先,讓我們在PHP 4.2中(也就是說關閉register_globals 選項)重寫上面提到的代碼:
<?php
$username = $_REQUEST['username'];
$password = $_REQUEST['password'];
// 檢查使用者名稱和口令
if ($username == 'kevin' and $password == 'secret')
$authorized = true;
?>
<?php if (!$authorized): ?>
<!-- 未授權的使用者將在這裡給予提示 -->
<p>Please enter your username and password:</p>
<form action="<?=$PHP_SELF?>" method="POST">
<p>Username: <input type="text" name="username" /><br />
Password: <input type="password" name="password" /><br />
<input type="submit" /></p>
</form>
<?php else: ?>
<!-- 有安全要求的HTML內容 -->
<?php endif; ?>
正如你看到的,我所需要做的只是在代碼的開始增加下面兩行:
$username = $_REQUEST['username'];
$password = $_REQUEST['password'];
因為我們希望使用者名稱和密碼是由使用者提交的,所以我們從$_REQUEST數組中擷取這些值。使用這個數組使得使用者可以自由選擇傳遞方式:通過URL查詢字串(例如允許使用者建立書籤時自動輸入他們的認證)、通過一個提交的表單或者是通過一個cookie。如果你想要限制只能通過表單提交認證(更精確地說,是通過HTTP POST請求),你可以使用$_POST數組:
$username = $_POST['username'];
$password = $_POST['password'];
除了“引入”這兩個變數以外,程式碼沒有任何改變。簡單地關閉register_globals選項促使開發人員更進一步瞭解哪些資料是來自外部的(不可信任的)資源。
請注意這裡還有一個小問題:PHP中預設的error_reporting設定仍然是E_ALL & ~E_NOTICE,因此如果“username”和“password”這兩個值沒有被提交,試圖從$_REQUEST數組或$_POST數組中獲得這兩個值並不會招致任何錯誤資訊。如晨不你的PHP程式需要嚴格的錯誤檢查,你還需要增加一些代碼以首先檢查這些變數。
但是這是不是意味著更多的輸入?
是的,在象上面這樣的簡單程式中,使用PHP 4.2常常會增加輸入量。但是,還是看看光明的一面吧 -- 你的程式終究是更安全了!
不過認真的說,PHP的設計者並沒有完全忽視你的痛苦。在這些新數組中有一個特殊的其它所PHP變數都不具備的特徵,它們是完全的全域變數。這對你有什麼協助呢?讓我們先對我們的樣本進行一下擴充。
為了使得網站中的多個頁面可以使用使用者名稱/口令論證,我們將我們使用者認證程式寫到一個include檔案(protectme.php)中:
<?php /* protectme.php */
function authorize_user($authuser, $authpass)
{
$username = $_POST['username'];
$password = $_POST['password'];
// 檢查使用者名稱和口令
if ($username != $authuser or $password != $authpass):
?>
<!-- 未授權的使用者將在這裡給予提示 -->
<p>Please enter your username and password:</p>
<form action="<?=$PHP_SELF?>" method="POST">
<p>Username: <input type="text" name="username" /><br />
Password: <input type="password" name="password" /><br />
<input type="submit" /></p>
</form>
<?php
exit();
endif;
}
?>
現在,我們剛才的頁面看上去將是這樣的:
<?php
require('protectme.php');
authorize_user('kevin','secret');
?>
<!-- 有安全要求的HTML內容 -->
很簡單,很清晰明了,對不對?現在是考驗你的眼力和經驗的時候了 -- 在authorize_user 函數中少了什嗎?
在函數中沒有申明$_POST是一個全域變數!在php 4.0中,當register_globals開啟時,你需要增加一行代碼以在函數中擷取$username和$password變數:
function authorize_user($authuser, $authpass)
{
global $username, $password;
...
在PHP中,和其它具有類似文法的語言不同,函數外的變數在函數中不能自動獲得,你需要象上面所說明的那樣增加一行以指定其來自global範圍。
在PHP 4.0中,當關閉register_globals以提供安全性時,你可以使用$HTTP_POST_VARS數組以獲得你的表單提交的值,但是你還是需要從全域範圍匯入這個數組:
function authorize_user($authuser, $authpass)
{
global $HTTP_POST_VARS;
$username = $HTTP_POST_VARS['username'];
$password = $HTTP_POST_VARS['password'];
但是在PHP 4.1及以後的版本中,特殊的$_POST變數(以及上面提到的其它變數)可以在所有範圍內使用。這就是不需要在函數中申明$_POST變數是一個全域變數的原因:
function authorize_user($authuser, $authpass)
{
$username = $_POST['username'];
$password = $_POST['password'];
這對session有什麼影響?
特殊的$_SESSION數組的引入實際上有助於簡化session代碼。你不需要將session變數申明為全域變數,然後再去留意哪些變數被註冊了,你現在可以簡單地從$_SESSION['varname']中引用你所有的session變數。
現在讓我們來看看另一個使用者認證的例子。這一次,我們使用sessions以標誌一個在你的網站繼續逗留的使用者已經經過了使用者認證。首先,我們來看看PHP 4.0版本(開啟register_globals):
<?php
session_start();
if ($username == 'kevin' and $password == 'secret')
{
$authorized = true;
session_register('authorized');
}
?>
<?php if (!$authorized): ?>
<!-- 顯示HTML表單以提示使用者登入 -->
<?php else: ?>
<!-- 有安全要求的HTML內容 -->
<?php endif; ?>
和剛開始的程式一樣,這個程式也存在安全性漏洞,在URL的最後加上?authorized=1可以繞過安全措施直接存取頁面內容。開發人員可以將$authorized視為一個session變數而忽視了可以很容易地通過使用者輸入設定同樣的變數。
當我們增加了我們的特殊的數組(PHP 4.1)並關閉register_globals(PHP 4.2)後,我們的程式將是這樣的:
<?php
session_start();
if ($username == 'kevin' and $password == 'secret')
$_SESSION['authorized'] = true;
?>
<?php if (!$_SESSION['authorized']): ?>
<!-- 顯示HTML表單以提示使用者登入 -->
<?php else: ?>
<!-- 有安全要求的HTML內容 -->
<?php endif; ?>
是不是更加簡單了?你不再需要再將普通的變數註冊為一個session變數,你只需要直接設定session變數(在$_SESSION數組中),然後用同樣的方法使用它。程式變得更短了,而且對於什麼變數是session變數也不會引起混亂!
總結
在這篇文章中,我解釋了PHP指令碼語言作出改變的深層原因。在PHP 4.1中,添加了一組特殊資料以訪問外部資料。這些數組可以在任何範圍內調用,這使得外部資料的訪問更方便。在PHP 4.2中,register_globals被預設關閉以鼓勵使用這些數組以避免無經驗的開發人員編寫出不安全的PHP代碼。