一、背景与核心风险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 })
}

发表评论 取消回复