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

}

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部