程式碼開發安全指南

From Joomla! Documentation

This page is a translated version of the page Secure coding guidelines and the translation is 98% complete.

Other languages:
English • ‎中文(台灣)‎

Joomla 包含了很多特色,可以協助在其上開發應用和擴充套件時的安全性。您應該要盡可能地一直使用這些特色功能,因為它們是已經被開發社群嘗試過、檢查過;而且可能還會在未來需要被更新。以下是關於使用 Joomla API最佳實踐的描述,確保您的擴充套件是盡可能安全的。

使用 request 來獲取資料

Joomla 3.x 從Joomla 3.0 版本開始, 應該停止使用JRequest,改成使用JInput


所有來自使用者發起的輸入都必須要被視為具有潛在危險的,並且必須要在使用前先做過清理。您應該總是使用 Joomla JInput class 來從請求擷取資料,而不是赤裸裸的 $_GET, $_POST 或是 $_REQUEST 變項,因為 JInput method 預設會過濾用戶的輸入。 JInput 會處理所有方面的用戶請求,讓它獨立於使用的 request method。 JInput 也可以用於獲取 cookie資料,甚至是伺服器和環境變數。然而,非常重要的是,使用正確的JInput method,來確保最大的安全性。使用 JInput->get method ,輔以預設參數是十分容易的,儘管在很多案例中,還可以對使用者的輸入套用更加嚴格的規範。

非常重要的是,要了解 JInput 方法並非 SQL-aware,而且需要額外作業來防範 SQL Injection。如果沒有在呼叫 JInput->get 當中指定預設,並沒有預設的值會被取回。假如沒有指定預設的話,而且沒有在請求變數當中顯示 argument,就會取回未定義(undefined)。

使用 JInput 也可以消除您需要留意 magic_quotes_gpc 設定的麻煩。無論您的 magic_quotes_gpc 有沒有打開,JInput 都恪盡職守。請查閱security.magicquotes.php 以獲得更多資訊。

考慮到用戶的輸入內容,您需要思考關於您要獲取的資料類型,並套用最嚴謹的 JInput 類型,依照每個不同的案例來應用。特定地說,避免使用像是JInput->get 的懶惰蟲手段,因為它會取回陣列,當中可能包含您不想要顯示的。儘管這些內容會被清理,它們經常在某些個別 argument 中,被額外的過濾器找到。例如,get 方法會把所有的 arguments 當成字串(string) 來處理,然而它們是可以被限制為整數之類的。

JInput 方法中頭三個參數 (parameter) 是相同的,只有頭一項 parameter 是強制的。一般而言,格式是

    JFactory::getApplication->input-><data-source>->get<type>( <name>, <default> )

其中

<type> 可以獲取的資料類型 (在底下查可以使用的類型)。
<name> 要獲取的變數(variable)的名稱(name) (例如,在 URL 當中 argument 的名稱name).
<default> 預設的值
<data-source> 指定變數要從何處來獲取 (見底下).

支援底下的 <data-source> 值:

get 由 URL 中 query 的部分送出的資料
post 從欄位 (field) 送出的資料。
method The same as either GET or POST depending on how the request was made.
cookie 在 cookies 中送出的資料。
request 所有的 GET, POST 以及 COOKIE 合併資料。這是預設的。
files 關於 POST request 檔案上傳的資訊
env 環境變數 (platform-specific)
server 網站伺服器變數 (platform-specific)

請注意預設會使用 REQUEST,其中包含了 cookie 資料。

以下的段落深入解釋每種資料類型。

整數

以下會接受整數。整數型可以包含一個前導的負號( - ),但正號( + )是不允許的。

$integer = JFactory::getApplication->input->getInt( 'id' );

會從 request 回傳 "id" argument 的值from the request (預設包含所有的 GET, POST and COOKIE data)。預設值是零

$integer = JFactory::getApplication->input->cookie->getInt( 'myId', 12 );

會從 cookie 回傳 "myId" 變數的值,預設值是12

浮點數

浮點數型可以包含一個前導的負號( - ),但正號( + )是不允許的。如果數字包含了千分位號,那千分位號前頭必須要至少有一個數字。例如,

$float = JFactory::getApplication->input->getFloat( 'price' );

會回傳請求的 'price' 參數,預設的值是 "0.0"

$float = JFactory::getApplication->input->post->getFloat( 'total', 100.00 );

會從 POST (而非 GET) 請求回傳 'total' 值。預設的值是 100.00

布林值

所有「非零」的值都會被視為 'true'。零是 'false'

$boolean = JFactory::getApplication->input->getBool( 'show' );

如果請求 'show' 參數的值是零,上面的程式會回傳 false;而如果是其他的參數,會回傳 1 (true)。預設的值是 false. 請注意任何字串都會造成回傳 true,所以使用URL包含"?show=false" 來呼叫上述的程式,事實上會回傳 true!

$boolean = JFactory::getApplication->input->get->getBool( 'hide', true );

會從 GET 請求 (而非 POST) 回傳 'hide' 參數的值。預設的值是 true.

Word

Word 的定義是指使用A到Z組成的字段。在 word 中使用部分小寫的字母是允許的。

$word = JFactory::getApplication->input->cookie->getWord( 'search-word' );

會獲取由request送出的 'search-word' 的值。預設是空白字串。

$word = JFactory::getApplication->input->cookie->getWord( 'keyword', '' );

會獲取 cookie 中 'keyword' 變數的值。預設是空白字串。

指令(Command)

指令(Command) 類似 word 但是允許的字元是更廣泛的。允許的字元包含:all alphanumeric characters, dot, dash (hyphen) and underscore.

$command = JFactory::getApplication->input->getCmd( 'option' );

會獲取由 request 請求回傳的 "option" 值。預設值是空字串。

$command = JFactory::getApplication->input->post->getCmd( 'controller', 'view' );

會獲取由 POST 請求(而非 GET) "controller" 參數的值。預設值是 'view'

字串

字串型允許更廣泛的字元輸入 It also takes an optional fourth argument specifying some additional mask options. 請查看 #Filter options for information on the available masks.

$string = JFactory::getApplication->input->getString( 'description' );

會獲取從 request 回傳 "description" 參數的值。預設的值是空字串。輸入的值會清除前後的空白,以及 HTML 標籤。

$string = JFactory::getApplication->input->getString( 'text', '' );

會獲取從 request 回傳 "text" 參數的值。預設的值是空字串。

$string = JFactory::getApplication->input->getString( 'template', '<html />' );

會從 request 回傳"template"參數的值。預設的值是 '<html></html>'.

JSON 字串

$json = JFactory::getApplication->input->json->get( 'var_name' );

通用及其他資料類型

如果上述的 methods 您要使用的,還有一些額外的過濾類型,可以直接呼叫 JInput->get method 。語法是:

JFactory::getApplication->input->get( <name>, <default>, <type> );

其中的:

<name> the name of the variable to be retrieved (for example, the name of an argument in a URL).
<default> 預設值。如果沒有在JInput->get 呼叫中指定預設值,則沒有預設值會被回傳。
<type> 指定的預期資料類型 (見底下)

The first three arguments are the same as for the more specific methods described earlier. Only the first argument is mandatory.

允許的 <type> 值,大小寫有分別,如底下:

INT, INTEGER 相當於 JInput->getInt.
UINT Get an unsigned integer. 相當於 JInput->getUint.
FLOAT, DOUBLE 相當於 JInput->getFloat.
BOOL, BOOLEAN 相當於 JInput->getBool.
WORD 相當於 JInput->getWord.
ALNUM 僅允許 alphanumeric 字元 (a-z, A-Z, 0-9). 相當於 JInput->getAlnum.
CMD 相當於 JInput->getCmd.
BASE64 僅允許可以被呈現於base64編碼的字元 (也就是 a-z, A-Z, 0-9, /, + 和 =)。相當於 JInput->getBase64.
STRING 相當於 JInput->getString.
ARRAY Source is not filtered but is cast to array type. When using this type you should use JFilterInput directly to filter the values in your data array.
HTML 經過安全處理的字串,相當於 JInput->getHtml.
PATH Valid pathname regex that filters out common attacks. 例如,any path beginning with a "/" will return an empty string. Simliarly, any path containing "/./" or "/../" will return an empty string. Dots within filenames are okay though. 相當於 JInput->getPath.
USERNAME 移除 control 字元 (0x00 - 0x1F), 0x7F, <, >, ", ', % and &. 相當於 JInput->getUsername.

過濾選項

所有的輸入值都應該要用 JFilterInput->clean 來過濾

過濾值矩陣

$data = JFactory::getApplication()->input->post->get('data', array(), 'array');
$filter = JFilterInput::getInstance();

foreach ($data as $value)
{
	$array[] = $filter->clean($value, 'string');
}

要看更多的過濾類型,請看 JFilterInput source.

底下列出的選項是過時的 JRequest library. 允許的 <options> 值羅列如下 (預設並不會套用任何一項):

JREQUEST_NOTRIM 並不會移除字串前後的空白
JREQUEST_ALLOWRAW 不會套用任何的過濾條件,請絕對謹慎使用!
JREQUEST_ALLOWHTML 不會移除字串中的 HTML

Masks can be combined by logically OR'ing them. 如果沒有指定過濾選項,那麼預設會移除空白,並且HTML會被移除。

上傳檔案

關於檔案上傳的安全性,網站伺服器已經提供了不錯的想法,然而,還是需要有些額外的手續,來確保檔案名稱以及路徑不會被濫用。一個簡化過的請求上傳檔案表單看起來像這樣:

<form action="index.php?option=com_mycomponent/form_handler.php"  method="post" enctype="multipart/form-data">
        <input type="file" name="Filedata" />
    <input type="submit" />
</form>

按下送出按鈕後,瀏覽器將會使用POST請求來上傳檔案,呼叫 Joomla! 的 "components/com_mycomponent/form_handler.php" 來傳送控制項目這包含了像是以下的程式碼。變項 $somepath 必須要被設定到路徑中,讓網頁伺服器有權限可以建立檔案。

// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die( 'Restricted access' );

// 從 request 獲得檔案資料矩陣
$file = JFactory::getApplication->input->get( 'Filedata', '', 'files', 'array' );

// 讓檔案名稱安全無害
jimport('joomla.filesystem.file');
$file['name'] = JFile::makeSafe($file['name']);

// 將上傳的檔案移動到常駐位置
if (isset( $file['name'] )) {

    // Make sure that the full file path is safe.
    $filepath = JPath::clean( $somepath.'/'.strtolower( $file['name'] ) );

    // 移動上傳的檔案
    JFile::upload( $file['tmp_name'], $filepath );
}

Saving a request variable into user state

Because setting a user state variable from a variable in the request is such a common operation, there is an API method to make the task easier. This is generally safe to use because it calls JInput->get to obtain the input from the request, but remember that none of the input filtering calls will protect against SQL injection attempts.

$app = JFactory::getApplication();
$app->getUserStateFromRequest( <key>, <name>, <default>, <type> );

where

<key> the name of the variable in the user state.
<name> the name of the request variable (same as the first argument of a JInput->get call).
<default> the default value to be assigned to the user state variable if the request variable is absent. The default is null.
<type> 預期的變數型別

例如,getting an integer variable called 'id' from the request with a default value of 0, then saving it into a session variable called 'myid' can be done like this:

$app = JFactory::getApplication();
$app->getUserStateFromRequest( 'myid', 'id, 0, 'int' );

instead of something like this:

$app = JFactory::getApplication();
$app->setUserState( 'myid', $app->input->getInt( 'id', 0 ) );

慎重結構 SQL queries

SQL Injection 是常見的網站攻擊類型之一,攻擊者會對輸入值管控不良的資料庫,修改資料庫請求的語法來破壞資料庫。攻擊結果會造成資料庫內容被破壞,或是隱私資料外洩。因此,確保SQL語法結構被送出時,能夠被正確地使用 escape 以及 quote 過濾,是很重要的,這樣惡意輸入才不會構成惡意的 SQL 語法。您將不能倚靠JInput 方法來完成這件事,因為它並非 SQL-aware。

強化整數以及其他數字值的安全

在 MySQL 資料庫中,數字欄位不應該被 quote,因此,將它們做類型轉換是很重要的。若是沒有這樣處理,您的程式碼會暴露於風險之下,讓駭客在 SQL 資料中存放字串。

依照其類型,數值類型看起來像這樣:

// For SQL data types: INT, INTEGER, TINYINT, SMALLINT, MEDIUMINT, BIGINT, YEAR
$query = 'SELECT * FROM #__table WHERE `id`=' . (int) $id;

// For SQL data types: FLOAT, DOUBLE
$query = 'SELECT * FROM #__table WHERE `id`=' . (float) $id;

總是對使用 <cdoe> JInput->getInt獲得的變數進行整數型別轉換,絕對是個好習慣。關於 SQL 注入攻擊更進一步的資訊,可以在 http://php.net/manual/en/security.database.sql-injection.php 以及 Retrieving_request_data_using_JInput#Getting_Values 找到。

強化字串安全

In the examples that follow it is assumed that $db is an instance of a Joomla database object. This can always be obtained from JFactory 使用

$db = JFactory::getDBO();

字串在使用於 SQL 語法之前,一定要先清理。這事實上是相當容易的,因為 JDatabase->quote method 已經為您清理所有的可疑內容了。您也可以直接使用 JDatabase->escape method。相當於底下的語法:

$query = 'SELECT * FROM #__table WHERE `field` = ' . $db->quote( $db->escape( $field ), false );

$query = 'SELECT * FROM #__table WHERE `field` = ' . $db->quote( $field );

強化搜尋安全

Special attention should be paid to LIKE clauses which contain the % wildcard character as these require special escaping in order to avoid possible denial of service attacks. LIKE clauses can be handled like this:

// Construct the search term by escaping the user-supplied string and, if required, adding the % wildcard characters manually.
$search = '%' . $db->escape( $search, true ) . '%';

// Construct the SQL query, being careful to suppress the default behaviour of Quote so as to prevent double-escaping.
$query = 'SELECT * FROM #__table WHERE `field` LIKE ' . $db->quote( $search, false );

強固資料安全

If data is to be entered into a datetime column then you can use the Joomla API to ensure a valid date format:

$date = JFactory::getDate( $mydate );
$query = 'UPDATE #__table SET `date` = ' . $db->quote( $date->toMySQL(), false );

Note that it is necessary to suppress database escaping as legitimate dates may contain characters that should not be escaped.

強固欄位名稱(field name)安全

In the comparatively rare case where a field name is a variable, that should also be quoted using an API call:

$query = 'SELECT * FROM #__table WHERE ' . $db->quoteName( $field->name ) . '=' . $db->quote( $field->value );

強固整數陣列安全

When you have an array of ids, typically used for IN() queries, you have to sanitise it also with JArrayHelper::toInteger($cid); before imploding:

...
$catId = JArrayHelper::toInteger($catId);
$query->where($db->quoteName('x.category_id') . ' IN (' . implode(',', $catId) . ')');

Quote 以及 QuoteName 的短別名

Shorter alternatives to the quote methods may also be used。的語法是相同意思的:

$query = 'SELECT * FROM #__table WHERE ' . $db->quoteName( $field->name ) . '=' . $db->quote( $field->value );
$query = 'SELECT * FROM #__table WHERE ' . $db->qn( $field->name ) . '=' . $db->q( $field->value );

強固表單安全

Apart from cleaning input variables as described above, you can also implement a simple technique which makes it more difficult for a cross-site request forgery attack (CSRF) to succeed. This involves adding a randomly-generated unique token to the form which is checked against a copy of the token held in the user's session. By checking that the submitted token matches the one contained in the stored session, it is possible to tie a rendered form to the request variables presented.

In POST forms you should add a hidden token field using:

echo JHTML::_( 'form.token' );

This outputs the token as a hidden form field looking like this:

<input type="hidden" name="8cb24ae69ffd7828ccecbcf06056e6fc" value="1" />

and places a copy of the token into the user's session, for later checking.

If you need to add the token to a URL rather than a form then you can use something like this:

echo JRoute::_( 'index.php?option=com_mycomponent&' . JSession::getFormToken() . '=1' );

In the most common scenario, you will want to check the token following a POST to the form handler. This can be done by adding this line of code to form handler:

JSession::checkToken() or die( JText::_( 'Invalid Token' ) );

If you need to pass the token in a GET request then you can check it like this:

JSession::checkToken( 'get' ) or die( JText::_( 'Invalid Token' ) );

In both cases the code will die if the token is omitted from the request, or the submitted token does not match the session token. If the token is correct but has expired, then JSession::checkToken will automatically redirect to the site front page.

清理檔案的系統路徑

If there is any possibility that a filesystem path might be constructed using data that originated from user input, then the path must be cleaned and checked before being used. 只要像這樣做,很容易就可以:

JPath::check( $path );

This will raise an error and terminate Joomla if the path contains a ".." or leads to a location outside the Joomla root directory. If you want to deal with the error yourself without terminating the application, then you can use code like this:

$path = JPath::clean( $path );
if (strpos( $path, JPath::clean( JPATH_ROOT ) ) !== 0) {
    // Handle the error here.
}

JPath:clean method 也可以使用於您的程式碼。它僅僅是移除首尾的空白字元,以 double slash (--) 代替;以標準的路徑分隔符號代替double slashes(//)

清理檔案路徑檔案名稱

As with filesystem paths, if there is any possibility that a file name might be constructed using user-originated data, then the file name must be cleaned and checked before use. 像這樣做就可以完成:

jimport('joomla.filesystem.file');
$clean = JFile::makeSafe( $unclean );

這個 method 會移除內容中的"." 或是多個"." 字元,以及其他非 alphabetic, numeric 或是dot, dash 或是 underscore 字元。如果字串起頭是".",也會被移除。