我正在开发一个“即插即用”系统,其中各个组件可以使用应用程序GUI注册并与上传的文件相关联。
但要真正“插件”播放“应用程序必须识别该组件,因为每个组件都是一个类,我可以通过使用接口来实现这一点。
但是如何验证上传文件的内容以搜索特定界面?
我的第一个想法是使用Tokenizer,但事实证明这比我想象的要难。一个简单的测试组件文件,如下所示:
<?php
class ValidComponent implements Serializable {
public serialize() {}
public unserialize( $serialized ) {}
}
经过token_get_all()传递后:
Array
(
[0] => Array
(
[0] => T_OPEN_TAG
[1] => <?php
[2] => 1
)
[1] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 2
)
[2] => Array
(
[0] => T_CLASS
[1] => class
[2] => 3
)
[3] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 3
)
[4] => Array
(
[0] => T_STRING
[1] => ValidComponent
[2] => 3
)
[5] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 3
)
[6] => Array
(
[0] => T_IMPLEMENTS
[1] => implements
[2] => 3
)
[7] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 3
)
[8] => Array
(
[0] => T_STRING
[1] => Serializable
[2] => 3
)
[9] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 3
)
[10] => U
[11] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 3
)
[12] => Array
(
[0] => T_PUBLIC
[1] => public
[2] => 5
)
[13] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 5
)
[14] => Array
(
[0] => T_STRING
[1] => serialize
[2] => 5
)
[15] => U
[16] => U
[17] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 5
)
[18] => U
[19] => U
[20] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 5
)
[21] => Array
(
[0] => T_PUBLIC
[1] => public
[2] => 6
)
[22] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 6
)
[23] => Array
(
[0] => T_STRING
[1] => unserialize
[2] => 6
)
[24] => U
[25] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 6
)
[26] => Array
(
[0] => T_VARIABLE
[1] => $serialized
[2] => 6
)
[27] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 6
)
[28] => U
[29] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 6
)
[30] => U
[31] => U
[32] => Array
(
[0] => T_WHITESPACE
[1] =>
[2] => 6
)
[33] => U
)
不仅效率不高,因为实际组件可能更大并且会产生巨大的阵列,但我不认为它非常可靠。
我当然可以使用这个结构并递归搜索它,寻找某个特定接口的名称但如果这个接口名出现在代码的任何地方(注释,常规字符串......),这肯定会给我一些误报。 )。
如果可能的话,我想避免使用文本比较或正则表达式,但我不知道是否可以创建一个隔离的沙箱来评估上传的文件以便使用Reflection。
答案 0 :(得分:1)
所以你想构建一个“系统”,用户可以上传PHP文件,而不是由所述系统使用?
除非您完全信任用户或在系统完全信任上传者的上下文中使用,例如在开发环境中,这是非常不安全的 ...
话虽如此,在没有运行的情况下分析php文件的最佳且可能唯一的MILDLY SANE方式是使用 tokenizer 。
例如,如果您只想知道该文件是否包含实现预定接口的类:
$source = file_get_contents('file.php');
$tokens = token_get_all($source);
function startsWithOpenTag($tokens)
{
return ($tokens[0][0] === T_OPEN_TAG);
}
function searchForInterface($tokens, $interfaceName)
{
$i = 0;
foreach ($tokens as $tk) {
if (isset($tk[1]) && strtolower($tk[1]) === 'implements') {
for ($ii = $i; $ii < count($tokens); ++$ii) {
if ($tokens[$ii] === '{') {
break;
} else {
if (isset($tokens[$ii][1]) && $tokens[$ii][2] === $interfaceName) {
return true;
}
}
}
}
++$i;
}
return false;
}
var_dump(startsWithOpenTag($tokens));
var_dump(searchForInterface($tokens, 'Serializable'));
这就够了。但是,这并不意味着文件中没有任何解析错误(或任何逻辑错误)。事实上,如果没有自己构建一个完整的PHP Parser(这是一种INSANE),那么确定文件有效的唯一方法就是运行它。
实现目标的最佳方法可能是创建PHP沙箱。您可以通过启动另一个PHP进程/线程来完成此操作。
Runkit是一个扩展,它提供了修改常量,用户定义函数和用户定义类的方法。它还通过沙盒提供自定义超全局变量和可嵌入的子解释器。
Runkit_Sandbox类创建一个具有自己的作用域和程序堆栈的新线程。使用传递给构造函数的一组选项,可以将此环境限制为主解释器可以执行的操作的子集,并为执行用户提供的代码提供更安全的环境。
例如,您可以通过打开另一个具有proc_open
或exec
的PHP进程来创建一种“沙箱”,该进程具有沙箱逻辑并负责解析和测试上载的文件。 / p>
在这个例子中,我们创建了3个文件:
查看可能有助于实现此目的的Symfony Console和Symfony Config组件。
$sandBoxWrapperPath = realpath('sandbox.php');
$uploadedFile = realpath('file.php');
$className = "\ValidComponent";
$command = "php \"$sandBoxWrapperPath\" -f \"$uploadedFile\" -c \"$className\"";
$descriptorspec = array(
1 => array("pipe", "w"), // STDOUT
2 => array("pipe", "w") // STDERR
);
$phpSandBox = proc_open($command, $descriptorspec, $pipes);
if (is_resource($phpSandBox)) {
$stdOut = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$stdErr = stream_get_contents($pipes[2]);
fclose($pipes[2]);
$exitCode = proc_close($phpSandBox);
echo "STDOUT: " . $stdOut . PHP_EOL . PHP_EOL;
echo "STDERR: " . $stdErr . PHP_EOL . PHP_EOL;
}
$shortopts = "";
$shortopts .= "f:"; // Uploaded File
$shortopts .= "c:"; // Name of the class, with namespace
$opts = getopt($shortopts);
if (!isset($opts['f'])) {
exit('File parameter is required');
}
// Instead, you can use tokenizer to pre parse the file.
// For instance, you can find class name this way
if (!isset($opts['c'])) {
exit('Class parameter is required');
}
$file = $opts['f'];
$className = $opts['c'];
require $file;
$refClass = new ReflectionClass($className);
//Do stuff with reflection
github上有几个PHP沙箱:
虽然这些项目似乎并不活跃......