From cdb026d7bc27176175a66789e5464efa63ab1dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=8B=A4=E6=B0=91?= Date: Fri, 19 Jun 2026 15:20:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E6=BC=8F=E6=B4=9E=20-=20=E5=91=BD=E4=BB=A4=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E9=98=B2=E6=8A=A4=E5=92=8C=20API=20=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 安全修复: - dsym.ts: 使用 execFileSync 替代 execSync,添加路径验证 - flutter.ts: 使用 execFileSync 替代 execSync,添加路径验证 - flutter.ts: 使用 crypto.randomBytes 生成唯一临时文件名 - symbolicate.ts: 添加 API Key 认证中间件 - symbolicate.ts: 添加输入长度限制(防 DoS) - 所有文件: 改用 ES module import 替代 require Co-Authored-By: Claude --- src/parsers/dsym.ts | 54 +++++++++++++++++++++++++++++--- src/parsers/flutter.ts | 65 +++++++++++++++++++++++++++++---------- src/routes/symbolicate.ts | 49 ++++++++++++++++++++++++++++- 3 files changed, 147 insertions(+), 21 deletions(-) diff --git a/src/parsers/dsym.ts b/src/parsers/dsym.ts index 0ad26ec..ba1be77 100644 --- a/src/parsers/dsym.ts +++ b/src/parsers/dsym.ts @@ -5,7 +5,7 @@ * atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -arch arm64 -l 0x102345000 0x102345678 */ -import { execSync } from 'child_process' +import { execFileSync } from 'child_process' import { StackFrame } from './stack-parser' export interface DsymConfig { @@ -17,6 +17,28 @@ export interface DsymConfig { arch?: string } +/** + * 验证路径安全性(防止命令注入) + */ +function validatePath(path: string): boolean { + // 只允许字母、数字、斜杠、点、下划线、连字符 + return /^[a-zA-Z0-9\/\.\_\-]+$/.test(path) +} + +/** + * 验证地址格式 + */ +function validateAddress(addr: string): boolean { + return /^0x[\da-fA-F]+$/.test(addr) +} + +/** + * 验证架构名称 + */ +function validateArch(arch: string): boolean { + return /^(arm64|x86_64|armv7|armv7s|i386)$/.test(arch) +} + /** * 使用 atos 命令符号化 iOS 堆栈 */ @@ -35,10 +57,34 @@ export function symbolicateWithDsym( return frames.map(f => `${f.function} (${f.file}:${f.line})`) } + // 验证输入参数(防止命令注入) + if (!validatePath(dsymPath)) { + console.error('Invalid dsymPath:', dsymPath) + return frames.map(f => `${f.function} (${f.file}:${f.line})`) + } + + if (!validateAddress(loadAddress)) { + console.error('Invalid loadAddress:', loadAddress) + return frames.map(f => `${f.function} (${f.file}:${f.line})`) + } + + if (!validateArch(arch)) { + console.error('Invalid arch:', arch) + return frames.map(f => `${f.function} (${f.file}:${f.line})`) + } + + // 验证所有地址格式 + for (const addr of addresses) { + if (!validateAddress(addr)) { + console.error('Invalid address:', addr) + return frames.map(f => `${f.function} (${f.file}:${f.line})`) + } + } + try { - // 调用 atos 命令 - const cmd = `atos -o "${dsymPath}" -arch ${arch} -l ${loadAddress} ${addresses.join(' ')}` - const output = execSync(cmd, { encoding: 'utf-8', timeout: 30000 }) + // 使用 execFileSync 避免 shell 注入(参数作为数组传递,不经过 shell) + const args = ['-o', dsymPath, '-arch', arch, '-l', loadAddress, ...addresses] + const output = execFileSync('atos', args, { encoding: 'utf-8', timeout: 30000 }) const symbolicated = output.trim().split('\n') let addrIndex = 0 diff --git a/src/parsers/flutter.ts b/src/parsers/flutter.ts index b196696..062442c 100644 --- a/src/parsers/flutter.ts +++ b/src/parsers/flutter.ts @@ -5,7 +5,11 @@ * 使用 flutter symbolize 命令进行符号化 */ -import { execSync } from 'child_process' +import { execFileSync } from 'child_process' +import { writeFileSync, readFileSync, unlinkSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import { randomBytes } from 'crypto' import { StackFrame } from './stack-parser' export interface FlutterDebugConfig { @@ -15,6 +19,22 @@ export interface FlutterDebugConfig { flutterPath?: string } +/** + * 验证路径安全性(防止命令注入) + */ +function validatePath(path: string): boolean { + // 只允许字母、数字、斜杠、点、下划线、连字符 + return /^[a-zA-Z0-9\/\.\_\-]+$/.test(path) +} + +/** + * 生成唯一的临时文件路径 + */ +function tempFile(suffix: string): string { + const random = randomBytes(8).toString('hex') + return join(tmpdir(), `flutter_${random}${suffix}`) +} + /** * 使用 flutter symbolize 命令符号化 Flutter 堆栈 */ @@ -24,30 +44,43 @@ export function symbolicateWithFlutter( ): string { const { debugSymbolsPath, flutterPath = 'flutter' } = config + // 验证路径安全性 + if (!validatePath(debugSymbolsPath)) { + console.error('Invalid debugSymbolsPath:', debugSymbolsPath) + return stacktrace + } + + if (!validatePath(flutterPath)) { + console.error('Invalid flutterPath:', flutterPath) + return stacktrace + } + + const tmpInput = tempFile('_stack.txt') + const tmpOutput = tempFile('_symbolicated.txt') + try { // 将堆栈写入临时文件 - const tmpInput = `/tmp/flutter_stack_${Date.now()}.txt` - const tmpOutput = `/tmp/flutter_symbolicated_${Date.now()}.txt` + writeFileSync(tmpInput, stacktrace) - require('fs').writeFileSync(tmpInput, stacktrace) - - // 调用 flutter symbolize - const cmd = `${flutterPath} symbolize -i "${tmpInput}" -o "${tmpOutput}" --debug-info "${debugSymbolsPath}"` - execSync(cmd, { timeout: 60000 }) + // 使用 execFileSync 避免 shell 注入 + const args = [ + 'symbolize', + '-i', tmpInput, + '-o', tmpOutput, + '--debug-info', debugSymbolsPath, + ] + execFileSync(flutterPath, args, { timeout: 60000 }) // 读取符号化结果 - const result = require('fs').readFileSync(tmpOutput, 'utf-8') - - // 清理临时文件 - try { - require('fs').unlinkSync(tmpInput) - require('fs').unlinkSync(tmpOutput) - } catch {} - + const result = readFileSync(tmpOutput, 'utf-8') return result } catch (err) { console.error('Flutter symbolization failed:', err) return stacktrace + } finally { + // 清理临时文件 + try { unlinkSync(tmpInput) } catch {} + try { unlinkSync(tmpOutput) } catch {} } } diff --git a/src/routes/symbolicate.ts b/src/routes/symbolicate.ts index 990b1b7..09a49bb 100644 --- a/src/routes/symbolicate.ts +++ b/src/routes/symbolicate.ts @@ -2,7 +2,7 @@ * 符号化 API 路由 */ -import { Router, Request, Response } from 'express' +import { Router, Request, Response, NextFunction } from 'express' import { parseStackFrames, Platform } from '../parsers/stack-parser' import { symbolicateWithSourcemap, formatSymbolicatedStack, SymbolicatedFrame } from '../parsers/sourcemap' import { parseProguardMapping, deobfuscateStack } from '../parsers/proguard' @@ -11,6 +11,29 @@ import { symbolicateWithFlutter } from '../parsers/flutter' export const symbolicateRouter = Router() +// API Key 认证中间件 +const API_KEY = process.env.SYMBOLICATOR_API_KEY || '' + +function authMiddleware(req: Request, res: Response, next: NextFunction) { + // 如果没有配置 API Key,则跳过认证(开发模式) + if (!API_KEY) { + return next() + } + + const providedKey = req.headers['x-api-key'] as string + if (!providedKey || providedKey !== API_KEY) { + return res.status(401).json({ + success: false, + error: 'Unauthorized: invalid or missing API key', + }) + } + + next() +} + +// 应用认证中间件 +symbolicateRouter.use(authMiddleware) + interface SymbolicateRequest { /** 平台类型 */ platform: Platform @@ -69,6 +92,30 @@ symbolicateRouter.post('/symbolicate', async (req: Request, res: Response) => { }) } + // 验证堆栈长度(防止 DoS) + if (stacktrace.length > 100000) { + return res.status(400).json({ + success: false, + error: 'stacktrace too long (max 100KB)', + }) + } + + // 验证 sourcemap 长度 + if (sourcemap && sourcemap.length > 50000000) { + return res.status(400).json({ + success: false, + error: 'sourcemap too long (max 50MB)', + }) + } + + // 验证 mappingFile 长度 + if (mappingFile && mappingFile.length > 100000000) { + return res.status(400).json({ + success: false, + error: 'mappingFile too long (max 100MB)', + }) + } + let result: { symbolicated: string; frames?: SymbolicatedFrame[] } switch (platform) {