1. 项目概述为什么今天还要深挖JSONP如果你是一名前端开发者或者对Web安全感兴趣那么“JSONP”这个词你一定不陌生。它曾经是跨域数据请求的救星如今却常被贴上“过时”、“不安全”的标签。但现实是在大量遗留系统、第三方数据服务乃至一些看似现代的API中JSONP的身影依然随处可见。更重要的是围绕JSONP的漏洞从简单的信息泄露到复杂的源码级逻辑缺陷构成了一个独特且富有挑战性的攻防战场。理解JSONP已经不仅仅是学习一段历史更是深入理解Web应用同源策略、客户端脚本执行机制以及逻辑漏洞挖掘的绝佳切入点。最近在安全社区和CTFCapture The Flag比赛中JSONP相关的漏洞频频出现从“手写jsonp”的实现挑战到模拟真实环境的“darkhole2服务器靶场攻防”再到“攻防世界”平台上的各类Web题目都揭示了这一技术背后潜藏的丰富攻防维度。它不像SQL注入或XSS那样直接粗暴更像是一场围绕信任边界和回调机制的“逻辑游戏”。本文将带你从零开始手写一个JSONP实现然后以此为跳板深入剖析其从应用层到源码级的各类漏洞原理、攻击手法及防御策略让你不仅能复现漏洞更能理解漏洞产生的根源。2. JSONP核心原理与手写实现2.1 同源策略的困境与JSONP的巧思浏览器同源策略Same-Origin Policy是Web安全的基石它阻止一个源的文档或脚本与另一个源的资源进行交互。这虽然安全但也给需要聚合多方数据的前端应用带来了麻烦。早期的开发者们发现script标签的src属性不受此限制可以加载任意域下的JavaScript文件。JSONPJSON with Padding正是利用了这一特性。它的核心思想是客户端定义一个回调函数然后将这个函数名作为参数通过script标签的src请求一个目标API。服务器端接收到请求后不是返回纯JSON而是将JSON数据作为参数“填充”到客户端指定的这个回调函数调用中返回一段可执行的JavaScript代码。浏览器加载这段脚本后就会自动执行这个回调函数从而实现了跨域获取数据。2.2 从零手写一个JSONP交互我们通过一个完整的例子来理解这个过程。假设我们有一个客户端页面client.html需要从api.example.com获取用户数据。第一步客户端发起请求客户端需要动态创建一个script标签并将回调函数名作为查询参数附加到请求URL上。!-- client.html 位于 http://localhost:8080 -- !DOCTYPE html html head title手写JSONP Demo/title /head body div idresult/div script // 1. 定义全局回调函数用于处理返回的数据 function handleUserData(data) { document.getElementById(result).innerHTML 用户名${data.name} 邮箱${data.email}; // 数据获取成功后清理动态创建的script标签 document.body.removeChild(script); } // 2. 动态创建script标签 const script document.createElement(script); // 将回调函数名作为callback参数传递给服务器 script.src http://api.example.com/user?callbackhandleUserData; // 3. 将script标签添加到文档中发起请求 document.body.appendChild(script); /script /body /html第二步服务器端响应服务器api.example.com需要解析callback参数并将数据包装成该参数指定的函数调用。这里我们用Node.js模拟一个简单的服务器// server.js const http require(http); const url require(url); const server http.createServer((req, res) { const parsedUrl url.parse(req.url, true); const pathname parsedUrl.pathname; if (pathname /user) { // 1. 获取客户端传来的回调函数名 const callbackName parsedUrl.query.callback || callback; // 2. 模拟要返回的数据 const userData { id: 1, name: 张三, email: zhangsanexample.com }; // 3. 关键步骤将数据填充到函数调用中返回JavaScript代码 const responseData ${callbackName}(${JSON.stringify(userData)}); // 设置正确的Content-Type res.writeHead(200, { Content-Type: application/javascript }); // 返回的是一段可执行的JS代码如handleUserData({id:1,name:张三,email:zhangsanexample.com}) res.end(responseData); } else { res.writeHead(404); res.end(Not Found); } }); server.listen(3000, () { console.log(JSONP Server running at http://localhost:3000/); });第三步执行流程浏览器加载client.html执行脚本创建script标签并设置src为http://api.example.com/user?callbackhandleUserData。向api.example.com发起GET请求。服务器接收到请求生成数据并包装成handleUserData({...})的字符串形式。浏览器收到这段JavaScript代码立即执行。由于handleUserData是全局函数因此成功被调用客户端页面就拿到了跨域的数据。注意JSONP只支持GET请求这是由其基于script标签的本质决定的。这也是它最大的局限性之一。2.3 手写实现中的关键细节与避坑指南在实际手写过程中有几个细节容易出错回调函数的管理回调函数必须挂载在全局对象window上。在复杂的单页应用SPA或模块化开发中要确保函数作用域正确。一个常见的技巧是使用随机函数名并在回调完成后清理。// 生成唯一回调函数名并自动清理 function jsonp(url, callback) { const callbackName jsonp_callback_${Date.now()}_${Math.random().toString(36).substr(2)}; window[callbackName] function(data) { callback(data); // 执行用户自定义的回调 // 清理移除全局函数和script标签 delete window[callbackName]; document.body.removeChild(script); }; const script document.createElement(script); script.src ${url}${url.includes(?) ? : ?}callback${callbackName}; document.body.appendChild(script); }错误处理原生JSONP没有像fetch或axios那样的错误处理机制。如果请求失败如404、超时回调函数永远不会被调用可能导致页面“卡住”。一个简单的改进是添加超时控制。function jsonp(url, callback, timeout 5000) { const callbackName jsonp_callback_${Date.now()}; let isTimeout false; const timer setTimeout(() { isTimeout true; cleanup(); console.error(JSONP request timeout); }, timeout); window[callbackName] function(data) { if (isTimeout) return; clearTimeout(timer); callback(data); cleanup(); }; function cleanup() { delete window[callbackName]; if (script.parentNode) { document.body.removeChild(script); } } const script document.createElement(script); script.onerror () { if (!isTimeout) { clearTimeout(timer); cleanup(); console.error(JSONP script load error); } }; script.src ${url}?callback${callbackName}; document.body.appendChild(script); }Content-Type服务器响应的Content-Type应该是application/javascript或text/javascript。如果错误地设置为application/json某些浏览器可能不会执行返回的脚本。3. JSONP常见安全漏洞剖析JSONP的灵活性带来了巨大的安全隐患因为它本质上是在目标域名下执行一段来自第三方的代码。下面我们深入几种典型的漏洞场景。3.1 回调函数名未过滤导致的XSS这是最经典也最危险的漏洞。如果服务器端对客户端传入的callback参数没有进行严格的过滤和验证攻击者可以注入任意JavaScript代码。漏洞示例 假设一个脆弱的服务器端代码如下// 危险直接拼接用户输入 const callback req.query.callback; const data {user: test}; const response ${callback}(${JSON.stringify(data)}); // 直接拼接 res.end(response);攻击者可以构造这样的请求http://vulnerable.com/api?callbackscriptalert(XSS)/script服务器返回scriptalert(XSS)/script({user:test})由于返回的Content-Type是application/javascript浏览器会将其作为脚本解析。虽然前面的script标签语法不完整但浏览器引擎在解析时可能会产生意想不到的执行效果。更常见的攻击是注入合法的JS代码例如http://vulnerable.com/api?callbackalert(1);//服务器返回alert(1);//({user:test})这会导致alert(1)立即执行后面的数据被注释掉。加固方案 服务器端必须对回调函数名进行严格的白名单验证或强格式检查。白名单只允许预定义的、安全的回调函数名如callback、handleResponse等但这限制了灵活性。强格式验证使用正则表达式确保回调函数名只包含字母、数字、下划线和美元符号且以字母或下划线开头符合JS标识符规范。function isValidCallbackName(name) { return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name); } if (!isValidCallbackName(callback)) { callback defaultCallback; }转义非字母数字字符更严格的做法是对于不在白名单内的字符进行过滤或编码但这可能影响正常使用。3.2 敏感信息泄露与越权访问JSONP端点可能无意中泄露敏感信息或者因为授权验证不严导致越权访问。场景一缺乏身份验证许多JSONP接口为了方便省略了身份验证如Cookie、Token认为回调函数机制本身是安全的。但攻击者可以在自己的恶意页面中直接调用该接口窃取任何访问该页面的用户的数据。场景二Referer检查绕过有些服务会检查HTTP请求头中的Referer字段确保请求来自“可信”的域名。然而Referer头可以被篡改在某些浏览器扩展或特殊请求中或者通过一些技巧如利用data:URL、本地文件使其为空或不可信值。更重要的是如果可信域名列表过于宽泛如*.example.com子域名下的任何一个XSS漏洞都可能被用来窃取主域的数据。场景三响应中包含敏感字段即使接口需要登录返回的JSON数据中也可能包含用户不应看到的其他字段如内部ID、哈希值、其他用户的关联信息。如果前端直接将这些数据传递给回调函数攻击者通过精心构造的恶意页面依然可以捕获到这些完整响应。实操心得在测试JSONP接口时不要只看它返回了什么更要看它在不同上下文如未登录状态、不同用户Cookie下会返回什么。尝试移除或修改Cookie、Referer等头部信息观察接口行为变化。3.3 基于JSONP的CSRF攻击虽然JSONP本身是“读”操作但结合其他漏洞可以间接实现“写”操作即CSRF跨站请求伪造。例如如果一个站点存在JSONP接口泄露了用户的CSRF Token攻击者就可以先通过JSONP窃取Token然后再用这个Token伪造一个状态变更请求如修改密码、转账。攻击链可能如下用户登录了bank.com。用户访问了恶意网站evil.com。evil.com的页面通过script标签请求bank.com/api/jsonp?callbackstealData该接口返回了包含CSRF Token的用户信息。恶意页面定义stealData函数获取到Token。恶意页面再用这个Token自动构造一个表单POST请求到bank.com/transfer完成转账。这个链条的关键在于JSONP漏洞泄露了用于防御CSRF的核心凭证。4. 源码级逻辑漏洞挖掘与攻防实战除了上述通用漏洞一些更隐蔽的问题隐藏在服务器处理JSONP请求的业务逻辑深处。这需要我们对服务器源码或通过黑盒测试推断逻辑有更深的理解。4.1 回调函数名处理逻辑缺陷我们来看一个比简单过滤更复杂的漏洞场景。假设服务器源码逻辑如下// 假设的服务器端源码片段 app.get(/api/user_info, (req, res) { let callback req.query.callback; const userData fetchUserData(req.session.userId); // 意图如果callback参数包含圆括号则只取括号前面的部分作为函数名 // 试图防止 callbackalert(1) 这种情况 if (callback callback.includes(()) { callback callback.split(()[0]; } // 然后它又“贴心”地帮用户补上括号和分号 const response ${callback}(${JSON.stringify(userData)});; res.type(js).send(response); });这段代码的本意是防御callbackalert(1)这种注入。攻击者输入callbackalert(1)被处理成callbackalert最终输出alert({...});看起来是安全的。但是攻击者可以这样绕过 构造参数callbackalert;console.log(1)//服务器处理流程callback值为alert;console.log(1)//。检查是否包含(不包含所以callback保持不变。拼接响应alert;console.log(1)//({...});浏览器执行alert;是一个合法的语句虽然无作用console.log(1)被执行//注释掉了后面的所有内容。攻击成功。漏洞根源开发者试图用简单的字符串处理来应对安全问题但逻辑不严谨且对JavaScript语法理解不足。安全的做法应该是前面提到的严格的白名单或标识符验证。4.2 参数污染与异常处理流程暴露JSONP接口有时会接收多个参数。服务器端可能根据参数是否存在或值的内容走不同的业务逻辑分支。攻击者可以通过参数污染Parameter Pollution来探测这些分支寻找未授权的数据访问路径。实战示例 假设一个接口/api/data?callbackcbid123typeprivate正常逻辑检查用户是否有权限查看id123的typeprivate数据。 攻击者尝试...id123typeprivatetypepublic传递两个type参数。服务器如何处理是取第一个、最后一个还是合并不同的处理方式可能导致权限检查被绕过。...id123type[]privatetype[]public尝试数组注入。...id123typePRIVATE大小写敏感吗。移除type参数...id123。服务器是否有一个默认的、权限检查更宽松的“公开”类型逻辑通过观察不同参数组合下返回数据的差异攻击者可以绘制出服务器的业务逻辑地图找到逻辑缺陷。4.3 JSONP与CORS错误配置的叠加风险现代Web应用通常使用CORS跨源资源共享来处理跨域请求。一个常见的错误配置是同时支持JSONP和CORS且CORS策略配置过于宽松。假设api.example.com有如下配置有一个JSONP端点/jsonp?callbackxxx有一个CORS端点/cors/dataCORS头部设置为Access-Control-Allow-Origin: *允许任何源攻击场景攻击者发现JSONP接口存在敏感信息泄露如CSRF Token。同时攻击者发现该域的CORS策略是*。攻击者可以编写一个恶意页面先通过JSONP窃取Token然后利用宽松的CORS策略直接使用fetch或XMLHttpRequest向/cors/data发起一个携带该Token的POST请求执行敏感操作。为什么JSONP放大了风险因为即使CORS配置为*浏览器在发起“非简单请求”如携带自定义头的POST请求时会先发一个OPTIONS预检请求。服务器需要正确响应预检请求攻击才能完成。然而如果攻击者已经通过JSONP拿到了有效的身份凭证如Token他可能可以构造一个“简单请求”如GET或特定格式的POST来绕过部分限制。JSONP在这里扮演了凭证窃取器的角色为后续的CORS滥用攻击铺平了道路。排查技巧在安全评估中如果发现目标同时存在JSONP接口和CORS端点一定要将它们结合起来分析。检查JSONP的响应是否包含可用于其他接口认证的令牌同时检查CORS策略的严格程度。这是一个经典的“112”的逻辑漏洞组合。5. 防御策略与安全开发实践理解了攻击手法防御的思路就清晰了。防御JSONP漏洞需要从服务器端和客户端如果可控同时入手。5.1 服务器端防御铁律废弃或严格限制JSONP在新的项目中绝对不要使用JSONP。全面转向CORS。对于历史遗留接口制定迁移计划。强制实施回调函数名白名单如果必须提供JSONP支持维护一个极小的、预定义的回调函数名白名单如callback、parseResponse拒绝任何不在名单内的请求。严格的输入验证与输出编码验证使用前面提到的强正则表达式验证回调函数名。长度也应限制如最多50个字符。编码即使验证通过在将回调函数名拼接到响应体中前可以考虑对非字母数字字符进行HTML实体编码或Unicode转义虽然可能破坏功能但安全优先。更好的做法是将回调函数名放在一个变量中而不是直接拼接。// 相对安全的做法使用函数名作为变量名 const response var callbackName ${callback}; if (window[callbackName]) { window[callbackName](${JSON.stringify(data)}); }; // 但这依然有风险如果callback是alert则window[“alert”]仍然存在。 // 最安全的还是白名单。实施完整的身份验证与授权JSONP接口必须和普通API接口一样检查会话Cookie、Token等凭证并验证当前用户是否有权访问所请求的数据。不能因为它是“脚本”就放松警惕。添加随机数Nonce或签名为每个合法的JSONP请求生成一个一次性令牌Nonce并在服务器端验证。或者对请求参数包括回调函数名进行签名防止参数被篡改。这能有效防御CSRF和参数污染攻击。设置严格的Content-Type始终设置Content-Type: application/javascript并考虑添加X-Content-Type-Options: nosniff头防止浏览器MIME类型嗅探导致的内容误解析。5.2 客户端调用方的自保措施如果你作为第三方需要调用不可控的JSONP服务可以采取以下措施降低风险使用沙盒iframe隔离将JSONP调用放在一个独立的、沙盒化的iframe中进行限制其权限。但这会带来通信复杂性。动态代理在自己的服务器端建立一个代理所有对第三方JSONP的请求都先发到自己的代理由代理服务器去获取数据清洗和验证后再以安全的方式如CORS返回给前端。这样将风险转移到了服务器端。超时与错误处理如前面手写实现所示必须实现严格的超时和错误处理机制避免脚本加载失败导致页面挂起。内容安全策略CSP部署严格的CSP通过script-src指令限制允许加载脚本的源。即使页面存在XSS攻击者也无法加载任意外部JSONP脚本来窃取数据。例如Content-Security-Policy: script-src self trusted.cdn.com;。注意这需要你明确知道所有合法的JSONP来源。5.3 安全审计与渗透测试要点在对含有JSONP功能的应用进行安全测试时可以遵循以下清单信息收集使用爬虫、JS文件分析等方式寻找callback、jsonp、jsoncallback等关键字。检查API文档、前端JS代码中是否存在JSONP调用模式。漏洞探测回调注入尝试callbackalert(1)//、callbackprompt(1)、callbackconsole.log、callback;alert(1);等多种Payload。敏感数据在登录和未登录状态下分别调用接口对比响应差异。尝试修改id、user_id等参数测试越权。Referer绕过使用工具如Burp Suite删除或修改Referer头或尝试从data:URL、本地文件发起请求。参数污染对每个参数进行重复、置空、特殊字符数组[]、.、%00等测试。CORS组合测试检查同一域名下是否存在配置不当的CORS端点。源码审计如果有条件重点审查处理callback参数的函数。审查JSONP接口的权限验证逻辑是否与普通API一致。审查响应拼接处是否存在未过滤的用户输入。JSONP是一面镜子它映照出Web开发早期在安全与功能间权衡的痕迹。今天尽管我们有更安全的CORS机制但理解JSONP及其漏洞能让我们更深刻地领悟“永远不要信任客户端输入”这一安全第一原则并在设计任何数据交换协议时对逻辑的严密性抱有更高的敬畏。在攻防的世界里那些看似过时的技术往往藏着最经典的逻辑陷阱。