我有一个接受 JS 插件的网络应用程序。也就是说,其他人编写的 JavaScript 代码,用户想要加载到我的应用程序中。
目前我正在使用 eval() 将他们的 JS 代码评估到运行时,但我知道这不安全。有没有比 eval() 更好的方法来做到这一点?
需要明确的是,用户将我指向一个干燥的文本文件(一个 URL),并且文件中的 JS 需要以某种方式变得生动。
我只知道动态导入 JS 脚本的两种方法:
<script>
标签问题的目的是弄清楚一种方法是否比另一种更安全,或者是否有比上述两种选择更好的方法。
这两者中的一个比另一个更安全吗?
不,从安全角度来看它们同样不好(好)。
它们在细节上有所不同,这会导致采用不同的方法来使它们更安全,但最终两者都会在您的环境中运行由不受信任的第三方编写的代码,并拥有其所有特权。这基本上是一个持续存在的 XSS 问题。
还有比上述2种更好的方法吗?
很多。这主要取决于这些插件应该在您的应用程序中做什么、谁编写它们以及谁安装(启用)它们。作为应用程序提供商和用户,您都不希望任意代码对用户的数据造成严重破坏。如果插件需要访问数据,您需要采取管理措施来确保只有受信任的代码才会运行,例如插件代码审核。至少您需要通知您的用户,他们在启用插件之前必须信任插件作者,这会给他们带来负担。此外,您还应该确保拥有可用日志,以防出现问题。
如果您确实想运行任意、不受信任的代码而不授予其访问用户数据的权限,您将需要考虑沙箱。有多种方法本质上是在代码无法脱离的虚拟机中执行。
对于 Chrome 扩展,我会专门使用 sandboxingEval,它允许加载可以在扩展托管的 iframe 中访问的沙盒文件。唯一的消息传递是通过普通的 iframe 消息传递。
例如,在
manifest.json
中声明要沙箱的页面:
{
...
"sandbox": {
"pages": [
"plugin.html"
]
"content_security_policy":
"sandbox allow-scripts; script-src 'self' https://plugin.com/"
],
...
}
确保外部域已列入白名单,以便可以嵌入。在 CSP 策略中,如果需要嵌入
allow-scripts
,则存在 <scripts>
。
现在在沙盒页面
plugin.html
中,使用外部脚本执行任何操作。在这种情况下,外部插件被下载,并通过消息传递将消息传递回扩展进程。
<!doctype html>
<html>
<head>
<script src="https://plugin.com/mohamedmansour/plugin.js"></script>
</head>
<body>
<script>
// Whatever my plugin contract is, lets send something back to our extension
// that the plugin initialized.
Plugin.do.something.here(() => {
window.postMessage({
name: 'CustomInitEvent',
data: 'initializing'
}, *);
});
// Listen from your extension plugin.html page some events.
window.addEventListener('message', (event) => {
var command = event.data.command;
switch(command) {
case 'CustomCommandA':
event.source.postMessage({
command: 'CustomCommandHello',
data: 'pong command a'
}, event.origin);
break;
}
});
</script>
</body>
</html>
现在在弹出窗口或任何地方,只需嵌入
plugin.html
。在这种情况下,popup.html 看起来像这样
<html>
<head>
<script src="plugin-manager.js"></script>
</head>
<body>
<iframe id="theFrame" src="plugin.html"></iframe>
</body>
</html>
然后你的
plugin-manager.js
负责控制插件。
const iframe = document.getElementById('theFrame');
window.addEventListener('message', function(event) {
switch(event.name) {
case 'CustomInitEvent':
console.log('Plugin Initialized');
break;
case 'CustomCommandHello':
console.log('Hey!');
break;
}
});
iframe.contentWindow.postMessage({
command: 'CustomCommandA'
});
iframe.contentWindow.postMessage({
command: 'CustomCommandB'
});
类似的事情。如果需要动态插件,只需将查询参数添加到 iframe 即可。在
plugin.html
内,只需动态添加脚本元素,然后这样调用即可:
<iframe id="theFrame" src="plugin.html?id=121212"></iframe>
您可能需要考虑的一个选项是在 WebAssembly 实例中运行插件。这有一些好处:
缺点是,争论所有这些低级技术可能有点棘手。这就是我推荐像 Extism 这样的开源项目的地方,以帮助平滑一些较低级别的细节:https://extism.org/。