前言
之前有功能需求,需要在前端页面上执行用户自定义的字符串js。直接的操作可以用eval
或者new Function
来执行字符串脚本。但是这样很不安全,获取cookie、获取隐私、发送请求等等代码块都有可能被恶意者故意注入进去。其实最好的方案,就是让后端去执行这段自定义脚本,返回结果给前端。当然,本次主要想解决的是除了这个方案,还有什么办法?网上找一番,可以较为安全地执行自定义脚本的方法有
- js解释器
- with + proxy
- 利用
iframe
的沙盒模式
利用js解释器,就是采用第三方包解释器来执行自定义脚本,如
const code = '1 + 2'
const interpreter = new Interpreter(code)
interpreter.run()
console.log(interpreter.value) // 3
with + proxy
就是在with
的scope
里执行自定义脚本,并且对with
语句指定对象进行代理,这样一来,使得with
语句里的代码块不会向上访问到外部的变量,达到避免自定义脚本恶意获取隐私的目的。具体参考
const sandbox = { a: 123 }
const sandboxProxy = new Proxy(sandbox, { has })
function has (target, key) {
return true
}
with(sandboxProxy) {
// code...
}
最后推荐使用iframe
的沙盒模式来执行自定义脚本,这可以真正达到约束脚本执行。大概实现思路就是,在需要执行自定义脚本的页面加入iframe
,并且设置sandbox
属性为allow-scripts
,利用message
通信,将需要执行的代码发给frame
页面,在该页面执行代码,并把结果返回给主页面。
利用iframe
的沙盒模式
主要的代码功能就是在主页面发送代码字符串给frame
页面
const frame = window.document.createElement('iframe')
frame.src = 'www.test.com/frame'
frame.sandbox = 'allow-scripts'
frame.style.display = 'none'
window.document.body.appendChild(frame)
// 发送自定义代码
const str = 'return 1 + 2'
frame.postMessage(str, '*')
等待接受执行结果
const reciveMessage = e => {
// 验证来源
if (e.origin === 'null' && e.source === frame.contentWindow) {
// 收到iframe的代码执行结果
console.log(e.data)
}
}
window.addEventListener('message', reciveMessage, false)
frame
页面执行代码,自定义代码若想获取cookie
、localStorage
等,都会报错
function reciveMessage(e) {
// 相当于window.top.currentWindow
const mainWindow = e.source
let result
try {
result = Function(e.data)()
} catch (err) {
result = '失败'
}
// 将结果返回给主页面
mainWindow.postMessage(result, e.origin)
}
window.addEventListener('message', reciveMessage, false)
具体代码
由于最近做的是react
项目,所以是用基于hook
写的示例
frame页面
// frame页面
import { useEffect } from 'react'
function reciveMessage(e) {
if (window.location.origin !== e.origin) {
return
}
// 相当于window.top.currentWindow
const mainWindow = e.source
let result
try {
result = Function(e.data)()
} catch (err) {
console.warn('失败', err)
result = '失败'
}
mainWindow.postMessage(result, e.origin)
}
export default function Frame() {
useEffect(() => {
window.addEventListener('message', reciveMessage, false)
return () => {
window.removeEventListener('message', reciveMessage, false)
}
}, [])
return null
}
主页面
// index页面
import { useEffect, useRef } from 'react'
function useScript(str, callback) {
const frameRef = useRef()
useEffect(() => {
if (str) {
if (!frameRef.current) {
// 建立iframe的沙盒模式
const frame = window.document.createElement('iframe')
frame.src = `${window.location.origin}/frame`
frame.sandbox = 'allow-scripts'
frame.style.display = 'none'
frameRef.current = frame
window.document.body.appendChild(frame)
}
const reciveMessage = e => {
// 进行信息来源的验证
if (e.origin === 'null' && e.source === frameRef.current.contentWindow) {
// 收到iframe的代码执行结果
callback(e.data)
}
}
window.addEventListener('message', reciveMessage, false)
return () => {
window.removeEventListener('message', reciveMessage, false)
}
}
}, [str, callback])
function onValidateScript(data) {
// 提供自定义代码需要的数据
const dataCode = `
"use strict";
var data = ${JSON.stringify(data)};
`
const iframe = frameRef.current
const iframeWin = iframe.contentWindow
// 向iframe发送字符串代码
iframeWin.postMessage(dataCode + str, '*')
}
return onValidateScript
}
export default function Index() {
const customCode = 'return data + 2'
const onValidateScript = useScript(
customCode,
data => console.log(data) // 3
)
return (
<button onClick={() => onValidateScript(1)}>执行脚本</button>
)
}
参考资料
https://www.imooc.com/article/17353
https://zhuanlan.zhihu.com/p/46571509