fix(代码执行): 修复沙箱引擎阻塞和超时问题

- 为 alert/confirm/prompt 提供 mock 实现,避免执行阻塞
- 为 DOM API 提供 fallback,防止元素不存在时报错
- 优化 TypeScript 转译,完善类型注解移除逻辑
- 添加 allow-same-origin 确保 postMessage 正常工作
- 改进危险 API 限制,提供友好错误提示
This commit is contained in:
gaoziman 2025-12-21 16:51:33 +08:00
parent 30156458ba
commit 19bac12c9b

View File

@ -61,23 +61,71 @@ export class SandboxEngine implements IExecutionEngine {
} }
private transpileTypeScript(code: string): string { private transpileTypeScript(code: string): string {
// 简单的 TypeScript 转换(移除类型注解) // TypeScript 转 JavaScript移除类型注解和 TS 特有语法)
// 实际项目中可以使用 @babel/standalone 或 typescript let result = code;
return code
// 移除类型注解 // 1. 移除接口定义(包括多行和 extends
.replace(/:\s*\w+(\[\])?(\s*[=,)])/g, '$2') result = result.replace(/\binterface\s+\w+(?:\s+extends\s+[\w,\s]+)?\s*\{[\s\S]*?\}/g, '');
// 移除接口定义
.replace(/interface\s+\w+\s*\{[^}]*\}/g, '') // 2. 移除类型别名
// 移除类型别名 result = result.replace(/\btype\s+\w+(?:<[^>]*>)?\s*=\s*[^;]+;/g, '');
.replace(/type\s+\w+\s*=\s*[^;]+;/g, '')
// 移除泛型 // 3. 移除 enum 定义
.replace(/<[^>]+>/g, '') result = result.replace(/\benum\s+\w+\s*\{[\s\S]*?\}/g, '');
// 移除 as 类型断言
.replace(/\s+as\s+\w+/g, '') // 4. 移除泛型参数(循环处理嵌套泛型)
// 移除 ! 非空断言 let prev = '';
.replace(/!\./g, '.') while (prev !== result) {
// 移除可选链前的类型 prev = result;
.replace(/\?\./g, '?.'); 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> { private executeInSandbox(code: string): Promise<string> {
@ -88,7 +136,10 @@ export class SandboxEngine implements IExecutionEngine {
// 创建隔离的 iframe // 创建隔离的 iframe
this.iframe = document.createElement('iframe'); this.iframe = document.createElement('iframe');
this.iframe.style.display = 'none'; this.iframe.style.display = 'none';
// 需要 allow-same-origin 才能让 postMessage 正常工作
// allow-scripts 允许执行脚本
this.iframe.sandbox.add('allow-scripts'); this.iframe.sandbox.add('allow-scripts');
this.iframe.sandbox.add('allow-same-origin');
document.body.appendChild(this.iframe); document.body.appendChild(this.iframe);
const iframeWindow = this.iframe.contentWindow; const iframeWindow = this.iframe.contentWindow;
@ -172,18 +223,87 @@ export class SandboxEngine implements IExecutionEngine {
// 替换全局 console // 替换全局 console
window.console = customConsole; window.console = customConsole;
// 限制危险 API // 提供浏览器 API 的 mock 实现(避免阻塞和错误)
delete window.fetch; window.alert = (msg) => {
delete window.XMLHttpRequest; customConsole.log('[Alert]', msg);
delete window.WebSocket; };
delete window.localStorage; window.confirm = (msg) => {
delete window.sessionStorage; 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.indexedDB;
delete window.open; window.open = () => { customConsole.log('[Blocked] window.open 在沙箱中不可用'); return null; };
delete window.close; window.close = () => { customConsole.log('[Blocked] window.close 在沙箱中不可用'); };
delete window.alert;
delete window.confirm;
delete window.prompt;
try { try {
// 执行用户代码 // 执行用户代码
@ -201,11 +321,11 @@ export class SandboxEngine implements IExecutionEngine {
})(); })();
`; `;
// 写入并执行 // 写入并执行(先写入 body 确保 document.body 存在)
const iframeDoc = this.iframe.contentDocument; const iframeDoc = this.iframe.contentDocument;
if (iframeDoc) { if (iframeDoc) {
iframeDoc.open(); iframeDoc.open();
iframeDoc.write(`<script>${sandboxCode}</script>`); iframeDoc.write(`<!DOCTYPE html><html><head></head><body><script>${sandboxCode}</script></body></html>`);
iframeDoc.close(); iframeDoc.close();
} }
}); });