一、背景与核心风险JSONP以`<script>`方式加载并在调用者上下文中直接执行,存在回调名注入与任意代码执行风险。缺乏`Content-Type`约束与`nosniff`防护,易受MIME嗅探与XSSI影响。依赖Cookie跨域时可能触发CSRF与会话泄露,且无法做细粒度权限与方法控制。缓存与CDN场景中易被投毒,缺少`Vary`与来源校验导致污染传播。二、风险识别与拦截策略拒绝`callback`参数并统一返回JSON;对历史接口进行网关级阻断与灰度迁移。若临时兼容,必须严格校验回调名并对返回值使用JSON前缀防XSSI,同时禁止携带敏感数据。统一开启`X-Content-Type-Options: nosniff`与精确`Content-Type`,并设置最小CSP限制。三、禁用JSONP与统一响应type Res = { setHeader: (k: string, v: string) => void; status: (n: number) => Res; end: (b?: string) => void } type Req = { query: Record<string, string | undefined> } function sendJson(res: Res, data: any) { res.setHeader('Content-Type', 'application/json; charset=utf-8') res.setHeader('X-Content-Type-Options', 'nosniff') res.end(JSON.stringify(data)) } function rejectJsonp(req: Req, res: Res) { const hasCallback = typeof req.query['callback'] === 'string' if (hasCallback) return res.status(400).end('jsonp_not_supported') } 四、临时兼容场景的严格校验(仅迁移期)function isSafeJsonpCallback(name: string): boolean { if (name.length > 128) return false const re = /^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/ return re.test(name) } function jsonWithPrefix(obj: any): string { const prefix = ")]}',\n" return prefix + JSON.stringify(obj) } function sendJsonp(res: Res, cb: string, data: any) { if (!isSafeJsonpCallback(cb)) return res.status(400).end('invalid_callback') res.setHeader('Content-Type', 'application/javascript; charset=utf-8') res.setHeader('X-Content-Type-Options', 'nosniff') res.end(`${cb}(${jsonWithPrefix(data)})`) } 五、CORS安全替代与精确配置type OriginCheck = (o: string) => boolean function corsHeaders(origin: string, check: OriginCheck) { const headers: Record<string, string> = {} if (check(origin)) { headers['Access-Control-Allow-Origin'] = origin headers['Vary'] = 'Origin' headers['Access-Control-Allow-Credentials'] = 'true' headers['Access-Control-Allow-Methods'] = 'GET,POST' headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization' headers['Access-Control-Max-Age'] = '600' } return headers } function applyCors(req: { headers: Record<string, string | undefined>; method: string }, res: Res, allow: Set<string>) { const origin = req.headers['origin'] || '' const hs = corsHeaders(origin, o => allow.has(o)) for (const [k, v] of Object.entries(hs)) res.setHeader(k, v) if (req.method === 'OPTIONS') return res.status(204).end() } 六、SSE与WebSocket作为推送替代import { ServerResponse } from 'http' function sse(res: ServerResponse) { res.setHeader('Content-Type', 'text/event-stream; charset=utf-8') res.setHeader('Cache-Control', 'no-cache') res.setHeader('X-Content-Type-Options', 'nosniff') res.write('retry: 5000\n') res.write(`data: ${JSON.stringify({ ok: true })}\n\n`) } type WsReq = { origin: string; token: string } function wsAllowedOrigin(req: WsReq, allow: Set<string>): boolean { return allow.has(req.origin) } function wsAuthorize(token: string): boolean { return /^[A-Za-z0-9_\-\.]{16,}$/.test(token) } 七、postMessage跨域整合type Msg = { type: string; payload?: any } function safePostMessage(target: Window, origin: string, message: Msg) { target.postMessage(message, origin) } function onMessage(ev: MessageEvent) { const allow = new Set(['https://example.com']) if (!allow.has(ev.origin)) return const msg = ev.data as Msg if (typeof msg?.type !== 'string') return } 八、迁移与验收步骤扫描并统计带`callback`参数的历史端点与调用方,建立清单与分级风险。网关拦截并返回`jsonp_not_supported`,前端改为`fetch`+CORS或SSE/WebSocket。验收项:无`callback`参数、`Content-Type`与`nosniff`正确、`Vary: Origin`设置、凭证跨域仅针对白名单来源。九、落地要点与参数校验清单回调名正则与长度限制已校验,最长128字符。`Access-Control-Allow-Origin`必须为精确匹配来源,不能使用`*`与携带凭证并存。`Access-Control-Max-Age`建议不超过`600`以降低策略漂移风险。SSE需设置`text/event-stream`与`nosniff`,并采用心跳与重试间隔控制。WebSocket需检查`Origin`与令牌格式,握手失败立即关闭。十、示例:统一接口处理流程function handle(req: Req & { headers: Record<string, string | undefined>; method: string }, res: Res) { rejectJsonp(req, res) applyCors({ headers: req.headers, method: req.method }, res, new Set(['https://example.com'])) sendJson(res, { ok: true }) }

发表评论 取消回复