最近更新: 2007-04-25

Reflection 於設計 Framework 時之安全性作用

Kuon 於《PHP5 的動態函數/行為調用效率測試》回應:不論是 Reflection 或是 Function Handling Functions, 其實都已經實作在某些 Framework 中, 像這種類的函數可怕在於其具有eval()的語意, 須特別注意安全性的議題

說實話,我抓不到這回覆內容的重點。依我的認知,於 PHP 中使用 Reflection 是為了增加安全性,而非帶來安全性議題(增加安全漏洞)。

PHP 的 Variable Function 並不具備 eval() 語意。 PHP 將代表函數名稱之變數的內容視為一個字串而非算式。如下列寫法就會發生錯誤:

<?php
$abc = 'file_put_contents("c:/test.txt", "ABC");';
echo $abc('123');
?>

Reflection 在 PHP 中由 extension (by C) 實現而非實作在框架(framework)中。事實上 PHP 要以框架形式實作 Reflaction 非常困難。要讀入類別源碼,然後分析源碼內容等等。我懷疑有多少人覺得在 PHP 中值得這麼做。如同我在 PHP5 的動態函數/行為調用效率測試 一文所說,PHP 的傳統方式就足以探知大部份的函數資訊了。我將 PHP 的傳統方式視為 PHP 語言原生的自識能力,而儘量不使用 PHP5 的 Reflection class (以自識能力為標準,PHP 作為動態語言的能力仍嫌不足。我同時認為它的 Reflection class 作法並不可取)。

再說說以 REST URL 形式 (http://host/class/method/arguments) 調用方法時,為什麼需要運用自識能力增加安全性。

我在設計框架時, controler 務必檢查類別源碼是否位在 URL 子目錄下 (或指定的目錄)。只有在指定目錄下的類別源碼,才視為此服務可用的類別。接著會在配置實例後,藉助 自識能力探知此實例是否具備指定方法以及相關的參數宣告。不過 PHP 的自識能力僅得以 is_callable() 檢查是否有指定方法可調用,而不能探知參數宣告資訊(PHP5 可以 Reflection class 進一步探知方法的參數資訊)。為其美中不足之處。

下列是一個非常簡單的實作品。假設其 URL 為 http://localhost/test/index.php 。它允許我以 http://localhost/test/index.php/class/method/argument 之 URL 形式調用個體方法。

<pre>
<?php
//require_once 'db.php';

class Db {
    public function query($sqlQuery) {
        echo $sqlQuery;
    }
}

if (!isset($_SERVER['PATH_INFO'])) {
    echo 'INDEX';
    exit(1);
}

echo $_SERVER['PATH_INFO'], "\n";
$pathinfoSet = explode('/', $_SERVER['PATH_INFO']);
array_shift($pathinfoSet);

$className = ucfirst(array_shift($pathinfoSet));
//check class name.

if (!ctype_alnum($className)) {
    echo "Class '{$className}' is not found.\n";
    exit(1);
}
echo 'Class: ', $className, "\n";

$methodName = array_shift($pathinfoSet);
echo 'Method: ', $methodName, "\n";

$arguments = $pathinfoSet;
echo 'Arguments: ', implode(',', $arguments), "\n";

//check class source file.

//Name convention: class X should be defined in X.php.

$classFilePath = dirname(__FILE__) . DIRECTORY_SEPARATOR
    . 'class' . DIRECTORY_SEPARATOR . $className . '.php';
if (!is_readable($classFilePath)) {
    echo "Class '{$className}' is not found.\n";
    exit(1);
}

//class should not be existed before required!

//if class is existed, PHP will raise a fatal error

//(Cannot redeclare class ...) when load class file.

require $classFilePath;

//is class defined in $classFilePath

if (!class_exists($className)) {
    echo "Class '{$className}' is not found.\n";
    exit(1);
}

$methodSet = array(new $className, $methodName);

//check method. Notice: I did not check arguments

if (!is_callable($methodSet)) {
    echo "Method {$methodName} is not found.\n";
    exit(1);
}

call_user_func_array($methodSet, $arguments);

?>
</pre>

假設 Db 是此框架的資料庫核心類別。若將第37-45行之檢查動作移除,使用者將可透過 http://localhost/test/index.php/db/query/select%20id%20from%20users 之 URL 讀取資料庫內容。故檢查動作不可省略。

在此稍微離題,提一下資料庫權限控制的資安議題。如果應用程式與資料庫系統所使用的權限控制系統是一致的,則讓使用者直接以 URL 查詢資料庫也無不可。因為應用程式與資料庫權限一致,當使用者以一般使用者的身份登入應用程式時,應用程式也同時以資料庫一般使用者的身份與資料庫建立連線。此時使用者直接查詢與透過應用程式查詢所能接觸的資料內容並無二致。然而現況為大多數應用程式以單一帳號與資料庫建立連線,由應用程式本身內建的規則管制使用者可得的資料內容。易言之,不論使用者是應用程式的匿名使用者或最高管理者,應用程式都以同一個資料庫帳號存取資料庫內容。使用者雖以不同帳號登入應用程式,但都具有相同的資料庫存取權限。

有許多現實因素造成了上述資料庫管理使用的資安窘境。例如虛擬主機商只提供有限的資料庫帳號。應用程式也有不得不接觸逾越使用者權限的資料內容之情況。例如使用者於應用程式端註冊新帳號時,勢必要以匿名身份存取使用者的帳號資料表。目前對此沒有什麼簡便的應對措施。 Case by case ,各顯神通。

言歸正傳。下列為一個針對上述實作之可用類別源碼。它必須置於指定的目錄下才可用。在此例中,它應以 X.php 為檔名,且位於 index.php 所在目錄之 class 子目錄中。使用者得透過 http://localhost/test/index.php/X/hello/john 之形式調用 – call $x->hello('john');

<?php
class X {
    public function hello($name='world') {
        echo 'hello ', $name;
    }
}
?>

就我看過的 PHP 框架而言,大抵都脫不了這類模式。我沒看過不作檢查就直接配置個體與調用方法的框架。雖然一個 PHP 初學者可能會犯這種錯誤,但一個有能力設計框架的 PHP 程序員應該都知道這些事項。至少在看過本文後,就知道哪些基本事項該注意了。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/3058219.html