一、风险与目标风险:越权字段访问、过度抓取与复杂查询导致资源耗尽、输入污染。目标:字段级Scope授权、统一深度与复杂度上限、操作名与持久化查询强制。
二、字段Scope映射与授权type Token = { sub: string; scope: string[] }
const fieldScope: Record<string, string> = {
'email': 'read:email',
'ssn': 'read:ssn',
'balance': 'read:balance'
}
function hasScope(tok: Token, need: string): boolean {
return tok.scope.includes(need)
}
function authorizeFields(tok: Token, fields: string[]): boolean {
for (const f of fields) {
const need = fieldScope[f]
if (need && !hasScope(tok, need)) return false
}
return true
}
三、查询深度与复杂度限制function maxDepth(query: string): number {
let depth = 0
let max = 0
for (const ch of query) {
if (ch === '{') { depth++; if (depth > max) max = depth }
else if (ch === '}') { depth = Math.max(0, depth - 1) }
}
return max
}
function estimateComplexity(query: string): number {
const m = query.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []
const ignore = new Set(['query','mutation','subscription','fragment','on','true','false'])
let count = 0
for (const w of m) if (!ignore.has(w)) count++
return count
}
四、字段提取与校验function extractFields(query: string, allow: string[]): string[] {
const out: string[] = []
for (const f of allow) {
const re = new RegExp(`\\b${f}\\b`)
if (re.test(query)) out.push(f)
}
return out
}
五、持久化查询与操作名type Persisted = { id: string; text: string }
function requireOperationName(name: string | undefined): boolean {
return typeof name === 'string' && name.length >= 3 && name.length <= 64
}
function matchPersisted(text: string, db: Record<string, Persisted>, id: string | undefined): boolean {
if (!id) return false
const item = db[id]
if (!item) return false
return item.text === text
}
六、统一拦截示例type Req = { body: { query: string; operationName?: string; persistedId?: string }; token: Token }
type Res = { status: (n: number) => Res; end: (b?: string) => void }
function guardGraphQL(req: Req, res: Res, persisted: Record<string, { id: string; text: string }>) {
const q = req.body.query || ''
const name = req.body.operationName
const id = req.body.persistedId
if (!requireOperationName(name)) return res.status(400).end('invalid_operation')
const depth = maxDepth(q)
if (depth > 8) return res.status(400).end('depth_exceeded')
const complexity = estimateComplexity(q)
if (complexity > 100) return res.status(400).end('complexity_exceeded')
if (!matchPersisted(q, persisted, id)) return res.status(400).end('unknown_query')
const fields = extractFields(q, Object.keys(fieldScope))
if (!authorizeFields(req.token, fields)) return res.status(403).end('forbidden')
}

发表评论 取消回复