问题😮
现在有一个脚本编辑器,它会在浏览器端运行js脚本,我们需要限制脚本来操作window、localStorage、发请求等操作,确保改网页下其他项目的安全。
解决方案👹
1、eval或者new Function()🤢
1、这样可以用来执行脚本,但是非常不安全,并没有对上面的操作进行限制。
2、eval是直接在局部环境执行,可以通过作用域链修改局部变量;
3、new Function()是在全局作用域执行,无法改局部变量,但是可以改全局变量。
new Function语法:
//let func = new Function ([arg1, arg2, ...argN], functionBody);
let sum = new Function('a', 'b', 'return a + b');
sum(1, 2) // 3
2、web worker🤠
如果脚本不涉及dom有关的内容,可以采用web worker构建沙箱环境。
场景
-
需要执行复杂的计算或数据处理
-
不需要 DOM 操作
-
希望提高应用的响应性
-
需要并行处理任务
-
可以在脚本中加载js脚本
特点
1、web Worker 无法直接访问 localStorage、sessionStorage 或其他 Window 对象的属性和方法。
2、可以通过重写fetch等方法对网络请求进行控制。
代码示例
index.html:
点击运行按钮,通过postMessage将脚本交给worker执行,当脚本执行fetch等操作时,会通过message将消息传递给主线程来执行,从而在主线程做限制。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="run()">运行</button>
<script>
const worker = new Worker('sandbox-worker.js');
// 运行
function run() {
let code = `console.log("Hello from sandbox");
//console.log(document);
//console.log(window.a);
//alert("This is a test alert");
fetch('http://localhost:3000/getData').then(response => console.log(response)).catch(error => console.error(error));
console.log('6666');
`
executeInSandbox(code)
}
function executeInSandbox(code) {
worker.postMessage({ type: 'sandbox-execute', code: code });
}
// 处理worker的消息,对console、alert、请求 做处理
worker.onmessage = function (event) {
switch (event.data.type) {
case 'log':
console.log.apply(console, event.data.data);
break;
case 'error':
throw event.data.data
break;
case 'warn':
console.warn.apply(console, event.data.data);
break;
case 'alert':
alert(event.data.data);
break;
case 'fetch':
// 处理 fetch 请求
handleFetch(event.data.id, event.data.url, event.data.options);
break;
}
};
function handleFetch(id, url, options = { method: 'get' }) {
// 实现 URL 过滤和请求处理
if (isAllowedUrl(url)) {
fetch(url, options)
.then(response => response.json())
.then(res => worker.postMessage({ type: 'fetchResult', data: { res, id } }))
.catch(error => worker.postMessage({ type: 'fetchError', error: error.message }));
} else {
worker.postMessage({ type: 'fetchError', error: 'URL not allowed' });
}
}
function isAllowedUrl(url) {
// 实现 URL 检查逻辑
// ...
if (url === 'http://localhost:3000/getData') {
return true
}
}
</script>
</body>
</html>
sandbox.js:
在worker中主要是执行脚本,拦截fetch、console等操作并交由主线程来执行。
由于postMessage不能传递函数,我们需要用map记录resolve函数和本次请求的id,并将id和请求信息传给主线程来请求,等有结果后再将id和结果传给worker线程,最后通过id获取resolve函数并执行,从而完成请求操作。
let requestId = 0
const pendingRequests = new Map()
self.onmessage = function (event) {
if (event.data.type === 'sandbox-execute') {
try {
const fn = new Function(event.data.code)
fn()
} catch (error) {
self.postMessage({ type: 'error', data: error })
}
} else if (event.data.type === 'fetchResult') {
const { id, res, error } = event.data.data
const request = pendingRequests.get(id)
if (request) {
if (error) {
request.reject(new Error(error))
} else {
request.resolve(res)
}
pendingRequests.delete(id)
}
}
}
// 重写importScripts加载脚本
const originalImportScripts = self.importScripts
self.importScripts = function (...urls) {
const allowedScripts = ['https://trusted-cdn.com/script1.js', 'https://trusted-cdn.com/script2.js']
for (const url of urls) {
if (!allowedScripts.includes(url)) {
throw new Error(`不允许加载脚本: ${url}`)
}
}
// 如果所有脚本都被允许,则调用原始的 importScripts
return originalImportScripts.apply(this, urls)
}
// 重写 console 方法
self.console = {
log: (...args) => self.postMessage({ type: 'log', data: args }),
error: (...args) => self.postMessage({ type: 'error', data: args }),
warn: (...args) => self.postMessage({ type: 'warn', data: args })
}
// 模拟 alert
self.alert = (message) => self.postMessage({ type: 'alert', data: message })
// 重写 fetch
self.fetch = (url, options) => {
return new Promise((resolve, reject) => {
// 需要一个机制来处理主线程的响应
const id = requestId++
pendingRequests.set(id, { resolve, reject })
self.postMessage({ type: 'fetch', url, options, id })
})
}
3、iframe-sandbox🤓
如果包含dom操作,脚本结果需要显示到浏览器上,这时就可以用iframe来做沙箱了。
场景
-
需要加载和隔离完整的第三方页面
-
需要在沙箱中进行 DOM 操作
-
要显示可视化内容
-
需要更严格的安全隔离
特点
1、通过设置sandbox属性,可以alert(allow-modals)等操作。
2、能阻止访问父页面的变量(allow-same-origin)。
【界面显示后用户再手动改sandbox属性时无效的】
3、要对请求拦截,可以重写fetch方法。
代码示例
index.html
通过postMessage给iframe脚本,并给iframe设置一些sandbox属性。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>安全的iframe脚本执行</title>
</head>
<body>
<iframe id="sandboxedFrame" src="./shandbox-iframe.html" style="position: fixed;top: 200px;left: 200px;"
sandbox="allow-scripts allow-popups"></iframe>
<button onclick="run()">运行</button>
<script>
function run() {
const iframe = document.getElementById('sandboxedFrame');
iframe.contentWindow.postMessage({
type: 'sandbox-run',
data: {
code: `
console.log(11) //可以console
// 创建一个新的 div 元素
const newDiv = document.createElement('div');
// 设置 div 的背景色为红色
newDiv.style.backgroundColor = 'red';
// 设置 div 的宽度和高度(可选)
newDiv.style.width = '100px';
newDiv.style.height = '100px';
// 将 div 添加到 body 中
document.body.appendChild(newDiv);
`
}
}, '*')
}
</script>
</body>
</html>
sandbox-iframe.html:
接收脚本,从新fetch方法,在new Function时进行传递,则在脚本中可以使用重写的fetch方法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>11</div>
<script>
window.addEventListener('message', (res) => {
console.log(res.data);
const { type, data } = res.data
if (type !== 'sandbox-run') {
return
}
// 保存原始的 fetch 方法
const originalFetch = window.fetch;
// 允许的源列表
const allowedOrigins = ['https://api.trusted-site.com', 'https://another-trusted-site.com'];
// 重写 fetch 方法
const safeFetch = function (url, options) {
const parsedUrl = new URL(url);
if (!allowedOrigins.includes(parsedUrl.origin)) {
return Promise.reject(new Error('不允许访问该源'));
}
return originalFetch(url, options);
};
// 使用 new Function 创建动态函数,并传递重写的 fetch 方法
const dynamicFunction = new Function('fetch', data.code);
// 调用动态函数,并传递重写的 fetch 方法
dynamicFunction(safeFetch);
})
</script>
</body>
</html>
sandbox属性
1. allow-forms:
- 允许 `iframe` 中的表单提交。
- 默认情况下,`iframe` 中的表单提交是被禁止的。
2. allow-modals:
- 允许使用模态窗口(如 `alert`、`prompt` 和 `confirm`)。
- 默认情况下,`iframe` 中的模态窗口是被禁止的。
3. **`allow-orientation-lock`**:
- 允许锁定屏幕方向。
- 默认情况下,`iframe` 中的内容不能锁定屏幕方向。
4. **`allow-pointer-lock`**:
- 允许使用 Pointer Lock API。
- 默认情况下,`iframe` 中的内容不能使用 Pointer Lock API。
5. **`allow-popups`**:
- 允许弹出窗口(如 `window.open`)。
- 默认情况下,`iframe` 中的弹出窗口是被禁止的。
6. **`allow-popups-to-escape-sandbox`**:
- 允许弹出窗口逃离沙箱限制。
- 默认情况下,`iframe` 中的弹出窗口也会受到沙箱限制。
7. **`allow-presentation`**:
- 允许使用 Presentation API。
- 默认情况下,`iframe` 中的内容不能使用 Presentation API。
8. **`allow-same-origin`**:
- 允许 `iframe` 中的内容被视为与主页面同源。
- 默认情况下,`iframe` 中的内容被视为不同源,不能访问主页面的内容。
9. **`allow-scripts`**:
- 允许执行脚本。
- 默认情况下,`iframe` 中的脚本执行是被禁止的。
10. **`allow-top-navigation`**:
- 允许 `iframe` 中的内容导航(加载)顶级浏览上下文。
- 默认情况下,`iframe` 中的内容不能导航顶级浏览上下文。
END🤡
我们可能还需要代码审查,日志记录等操作,但是这些技术只是防君子不防小人,哈哈。😹