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>
这个提交包含在:
父节点
dc3018d7af
当前提交
cdb026d7bc
@ -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
|
||||
|
||||
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户