fix: 修复安全漏洞 - 命令注入防护和 API 认证

安全修复:
- 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 <noreply@anthropic.com>
这个提交包含在:
徐勤民 2026-06-19 15:20:31 +08:00
父节点 dc3018d7af
当前提交 cdb026d7bc
共有 3 个文件被更改,包括 147 次插入21 次删除

查看文件

@ -5,7 +5,7 @@
* atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -arch arm64 -l 0x102345000 0x102345678 * 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' import { StackFrame } from './stack-parser'
export interface DsymConfig { export interface DsymConfig {
@ -17,6 +17,28 @@ export interface DsymConfig {
arch?: string 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 * 使 atos iOS
*/ */
@ -35,10 +57,34 @@ export function symbolicateWithDsym(
return frames.map(f => `${f.function} (${f.file}:${f.line})`) 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 { try {
// 调用 atos 命令 // 使用 execFileSync 避免 shell 注入(参数作为数组传递,不经过 shell
const cmd = `atos -o "${dsymPath}" -arch ${arch} -l ${loadAddress} ${addresses.join(' ')}` const args = ['-o', dsymPath, '-arch', arch, '-l', loadAddress, ...addresses]
const output = execSync(cmd, { encoding: 'utf-8', timeout: 30000 }) const output = execFileSync('atos', args, { encoding: 'utf-8', timeout: 30000 })
const symbolicated = output.trim().split('\n') const symbolicated = output.trim().split('\n')
let addrIndex = 0 let addrIndex = 0

查看文件

@ -5,7 +5,11 @@
* 使 flutter symbolize * 使 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' import { StackFrame } from './stack-parser'
export interface FlutterDebugConfig { export interface FlutterDebugConfig {
@ -15,6 +19,22 @@ export interface FlutterDebugConfig {
flutterPath?: string 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 * 使 flutter symbolize Flutter
*/ */
@ -24,30 +44,43 @@ export function symbolicateWithFlutter(
): string { ): string {
const { debugSymbolsPath, flutterPath = 'flutter' } = config 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 { try {
// 将堆栈写入临时文件 // 将堆栈写入临时文件
const tmpInput = `/tmp/flutter_stack_${Date.now()}.txt` writeFileSync(tmpInput, stacktrace)
const tmpOutput = `/tmp/flutter_symbolicated_${Date.now()}.txt`
require('fs').writeFileSync(tmpInput, stacktrace) // 使用 execFileSync 避免 shell 注入
const args = [
// 调用 flutter symbolize 'symbolize',
const cmd = `${flutterPath} symbolize -i "${tmpInput}" -o "${tmpOutput}" --debug-info "${debugSymbolsPath}"` '-i', tmpInput,
execSync(cmd, { timeout: 60000 }) '-o', tmpOutput,
'--debug-info', debugSymbolsPath,
]
execFileSync(flutterPath, args, { timeout: 60000 })
// 读取符号化结果 // 读取符号化结果
const result = require('fs').readFileSync(tmpOutput, 'utf-8') const result = readFileSync(tmpOutput, 'utf-8')
// 清理临时文件
try {
require('fs').unlinkSync(tmpInput)
require('fs').unlinkSync(tmpOutput)
} catch {}
return result return result
} catch (err) { } catch (err) {
console.error('Flutter symbolization failed:', err) console.error('Flutter symbolization failed:', err)
return stacktrace return stacktrace
} finally {
// 清理临时文件
try { unlinkSync(tmpInput) } catch {}
try { unlinkSync(tmpOutput) } catch {}
} }
} }

查看文件

@ -2,7 +2,7 @@
* API * API
*/ */
import { Router, Request, Response } from 'express' import { Router, Request, Response, NextFunction } from 'express'
import { parseStackFrames, Platform } from '../parsers/stack-parser' import { parseStackFrames, Platform } from '../parsers/stack-parser'
import { symbolicateWithSourcemap, formatSymbolicatedStack, SymbolicatedFrame } from '../parsers/sourcemap' import { symbolicateWithSourcemap, formatSymbolicatedStack, SymbolicatedFrame } from '../parsers/sourcemap'
import { parseProguardMapping, deobfuscateStack } from '../parsers/proguard' import { parseProguardMapping, deobfuscateStack } from '../parsers/proguard'
@ -11,6 +11,29 @@ import { symbolicateWithFlutter } from '../parsers/flutter'
export const symbolicateRouter = Router() 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 { interface SymbolicateRequest {
/** 平台类型 */ /** 平台类型 */
platform: Platform 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[] } let result: { symbolicated: string; frames?: SymbolicatedFrame[] }
switch (platform) { switch (platform) {