fix(代码执行): 修复沙箱引擎阻塞和超时问题
- 为 alert/confirm/prompt 提供 mock 实现,避免执行阻塞 - 为 DOM API 提供 fallback,防止元素不存在时报错 - 优化 TypeScript 转译,完善类型注解移除逻辑 - 添加 allow-same-origin 确保 postMessage 正常工作 - 改进危险 API 限制,提供友好错误提示
This commit is contained in:
parent
30156458ba
commit
19bac12c9b
@ -61,23 +61,71 @@ export class SandboxEngine implements IExecutionEngine {
|
||||
}
|
||||
|
||||
private transpileTypeScript(code: string): string {
|
||||
// 简单的 TypeScript 转换(移除类型注解)
|
||||
// 实际项目中可以使用 @babel/standalone 或 typescript
|
||||
return code
|
||||
// 移除类型注解
|
||||
.replace(/:\s*\w+(\[\])?(\s*[=,)])/g, '$2')
|
||||
// 移除接口定义
|
||||
.replace(/interface\s+\w+\s*\{[^}]*\}/g, '')
|
||||
// 移除类型别名
|
||||
.replace(/type\s+\w+\s*=\s*[^;]+;/g, '')
|
||||
// 移除泛型
|
||||
.replace(/<[^>]+>/g, '')
|
||||
// 移除 as 类型断言
|
||||
.replace(/\s+as\s+\w+/g, '')
|
||||
// 移除 ! 非空断言
|
||||
.replace(/!\./g, '.')
|
||||
// 移除可选链前的类型
|
||||
.replace(/\?\./g, '?.');
|
||||
// TypeScript 转 JavaScript(移除类型注解和 TS 特有语法)
|
||||
let result = code;
|
||||
|
||||
// 1. 移除接口定义(包括多行和 extends)
|
||||
result = result.replace(/\binterface\s+\w+(?:\s+extends\s+[\w,\s]+)?\s*\{[\s\S]*?\}/g, '');
|
||||
|
||||
// 2. 移除类型别名
|
||||
result = result.replace(/\btype\s+\w+(?:<[^>]*>)?\s*=\s*[^;]+;/g, '');
|
||||
|
||||
// 3. 移除 enum 定义
|
||||
result = result.replace(/\benum\s+\w+\s*\{[\s\S]*?\}/g, '');
|
||||
|
||||
// 4. 移除泛型参数(循环处理嵌套泛型)
|
||||
let prev = '';
|
||||
while (prev !== result) {
|
||||
prev = result;
|
||||
result = result.replace(/<[^<>]+>/g, '');
|
||||
}
|
||||
|
||||
// 5. 移除访问修饰符(private, public, protected, readonly)
|
||||
result = result.replace(/\b(private|public|protected)\s+/g, '');
|
||||
result = result.replace(/\breadonly\s+/g, '');
|
||||
|
||||
// 6. 移除函数/方法返回类型注解
|
||||
// ): string { -> ) {
|
||||
// ): string => -> ) =>
|
||||
// ): Promise<void> { -> ) { (泛型已在步骤4移除)
|
||||
result = result.replace(/\)\s*:\s*[\w\[\]|&\s]+\s*(\{|=>)/g, ') $1');
|
||||
|
||||
// 7. 移除函数参数类型注解(循环处理多参数)
|
||||
// (person: Person) -> (person)
|
||||
// (name: string, age: number) -> (name, age)
|
||||
// (value?: string) -> (value)
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const before = result;
|
||||
result = result.replace(/(\(|,)\s*(\w+)\s*\??\s*:\s*[\w\[\]|&\s]+([,)])/g, '$1$2$3');
|
||||
if (before === result) break;
|
||||
}
|
||||
|
||||
// 8. 移除类属性/变量类型注解
|
||||
// greeting: string; -> greeting;
|
||||
// count: number = 0; -> count = 0;
|
||||
result = result.replace(/(\w+)\s*:\s*[\w\[\]|&\s]+\s*([;=])/g, '$1 $2');
|
||||
|
||||
// 9. 移除 as 类型断言
|
||||
result = result.replace(/\s+as\s+[\w\[\]]+/g, '');
|
||||
|
||||
// 10. 移除 ! 非空断言
|
||||
result = result.replace(/!([.;,)\s])/g, '$1');
|
||||
result = result.replace(/!\./g, '.');
|
||||
|
||||
// 11. 移除 declare 关键字
|
||||
result = result.replace(/\bdeclare\s+/g, '');
|
||||
|
||||
// 12. 移除 abstract 关键字
|
||||
result = result.replace(/\babstract\s+/g, '');
|
||||
|
||||
// 13. 移除 implements 子句
|
||||
result = result.replace(/\s+implements\s+[\w,\s]+(?=\s*\{)/g, '');
|
||||
|
||||
// 14. 清理多余的空行
|
||||
result = result.replace(/^\s*[\r\n]/gm, '\n');
|
||||
result = result.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
private executeInSandbox(code: string): Promise<string> {
|
||||
@ -88,7 +136,10 @@ export class SandboxEngine implements IExecutionEngine {
|
||||
// 创建隔离的 iframe
|
||||
this.iframe = document.createElement('iframe');
|
||||
this.iframe.style.display = 'none';
|
||||
// 需要 allow-same-origin 才能让 postMessage 正常工作
|
||||
// allow-scripts 允许执行脚本
|
||||
this.iframe.sandbox.add('allow-scripts');
|
||||
this.iframe.sandbox.add('allow-same-origin');
|
||||
document.body.appendChild(this.iframe);
|
||||
|
||||
const iframeWindow = this.iframe.contentWindow;
|
||||
@ -172,18 +223,87 @@ export class SandboxEngine implements IExecutionEngine {
|
||||
// 替换全局 console
|
||||
window.console = customConsole;
|
||||
|
||||
// 限制危险 API
|
||||
delete window.fetch;
|
||||
delete window.XMLHttpRequest;
|
||||
delete window.WebSocket;
|
||||
delete window.localStorage;
|
||||
delete window.sessionStorage;
|
||||
// 提供浏览器 API 的 mock 实现(避免阻塞和错误)
|
||||
window.alert = (msg) => {
|
||||
customConsole.log('[Alert]', msg);
|
||||
};
|
||||
window.confirm = (msg) => {
|
||||
customConsole.log('[Confirm]', msg);
|
||||
return true; // 默认返回 true
|
||||
};
|
||||
window.prompt = (msg, defaultValue) => {
|
||||
customConsole.log('[Prompt]', msg);
|
||||
return defaultValue || '用户输入'; // 返回默认值或模拟值
|
||||
};
|
||||
|
||||
// Mock document API
|
||||
const mockElement = {
|
||||
innerHTML: '',
|
||||
innerText: '',
|
||||
textContent: '',
|
||||
value: '',
|
||||
style: {},
|
||||
classList: {
|
||||
add: () => {},
|
||||
remove: () => {},
|
||||
toggle: () => {},
|
||||
contains: () => false,
|
||||
},
|
||||
appendChild: () => mockElement,
|
||||
removeChild: () => mockElement,
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
setAttribute: () => {},
|
||||
getAttribute: () => null,
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
|
||||
// 保存原始 document 方法
|
||||
const originalGetElementById = document.getElementById.bind(document);
|
||||
const originalQuerySelector = document.querySelector.bind(document);
|
||||
const originalQuerySelectorAll = document.querySelectorAll.bind(document);
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
|
||||
// 重写 document 方法,提供 fallback
|
||||
document.getElementById = (id) => {
|
||||
const el = originalGetElementById(id);
|
||||
if (!el) {
|
||||
customConsole.log('[DOM] getElementById("' + id + '") 返回 null,使用 mock 元素');
|
||||
return mockElement;
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
document.querySelector = (selector) => {
|
||||
const el = originalQuerySelector(selector);
|
||||
if (!el) {
|
||||
return mockElement;
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
document.querySelectorAll = (selector) => {
|
||||
return originalQuerySelectorAll(selector) || [];
|
||||
};
|
||||
|
||||
document.createElement = (tag) => {
|
||||
try {
|
||||
return originalCreateElement(tag);
|
||||
} catch {
|
||||
return mockElement;
|
||||
}
|
||||
};
|
||||
|
||||
// 限制危险的网络和存储 API
|
||||
window.fetch = () => Promise.reject(new Error('fetch 在沙箱中不可用'));
|
||||
window.XMLHttpRequest = undefined;
|
||||
window.WebSocket = undefined;
|
||||
window.localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {} };
|
||||
window.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {} };
|
||||
delete window.indexedDB;
|
||||
delete window.open;
|
||||
delete window.close;
|
||||
delete window.alert;
|
||||
delete window.confirm;
|
||||
delete window.prompt;
|
||||
window.open = () => { customConsole.log('[Blocked] window.open 在沙箱中不可用'); return null; };
|
||||
window.close = () => { customConsole.log('[Blocked] window.close 在沙箱中不可用'); };
|
||||
|
||||
try {
|
||||
// 执行用户代码
|
||||
@ -201,11 +321,11 @@ export class SandboxEngine implements IExecutionEngine {
|
||||
})();
|
||||
`;
|
||||
|
||||
// 写入并执行
|
||||
// 写入并执行(先写入 body 确保 document.body 存在)
|
||||
const iframeDoc = this.iframe.contentDocument;
|
||||
if (iframeDoc) {
|
||||
iframeDoc.open();
|
||||
iframeDoc.write(`<script>${sandboxCode}</script>`);
|
||||
iframeDoc.write(`<!DOCTYPE html><html><head></head><body><script>${sandboxCode}</script></body></html>`);
|
||||
iframeDoc.close();
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user