资讯动态

安全响应头缺失与CORS跨域漏洞检测:Python批量扫描工具实战

发布时间:2026/10/1 2:58:27 来源:尧图企业网站定制
简介面向网络开发与安全测试场景该工具提供了针对HTTP请求头缺失与CORS跨域漏洞的检测方案。它通过模拟GET、POST、PUT、DELETE等请求分析服务器响应头帮助开发者、安全工程师快速定位Content-Type、Authorization、Referer、Cookie等常见头配置问题并审计Access-Control-Allow-Origin、Allow-Methods、Allow-Credentials等关键CORS策略覆盖开发阶段调试、生产环境定期安全审计与渗透测试等典型用途。压缩包仅3KB共3个文件Python主脚本用于自动化扫描与报告md文档说明原理与使用流程txt列出依赖项结构简洁便于直接运行。该工具已吸引857人学习适合希望快速排查Web服务安全配置、优化CORS策略并提升整体安全性的初中级技术人员。1. 为什么要把“检测HTTP请求头缺失和CORS跨域漏洞”做成一个独立工具假设你负责一个前后端分离项目的上线自测或者刚接到一个Web应用的授权测试任务最容易被忽略也最不该忽略的两件事就是HTTP请求头缺失和CORS跨域漏洞。这两类问题的共同点是功能测试阶段完全无感页面正常渲染、接口正常调用只有到安全评审时才会被翻出来。手动用浏览器F12逐条看响应头十几个URL可以接受几十个接口就会看到麻木还容易漏。标题里的工具做的就是批量扫描一批URL自动判断安全响应头缺了哪些、CORS配置是否允许跨源读取。适合三类人前端团队上线前自测、安全人员做快速基线扫描、运维在网关调整后做回归验证。2. HTTP请求头缺失检测从基线清单到能跑的Python脚本2.1 该查哪些响应头直接把这份基线清单抄走响应头缺失检测的难点不在写代码而在定基线。OWASP官方列出的安全响应头有十几个直接全量上会导致误报爆炸。我的做法是分三档高、中、低低档只记录不阻断。下面这份清单是我在实际项目里沉淀下来、可以直接抄走的版本响应头严重级推荐值缺失的实际影响Strict-Transport-Security高max-age31536000; includeSubDomains用户首次访问走HTTP时被降级存在会话被拦截的风险Content-Security-Policy高default-src self一旦注入脚本CSP是最后一道能挡住大多数利用路径的墙X-Frame-Options高DENY 或 SAMEORIGIN页面可被恶意嵌套iframe做点击劫持登录页风险尤其大X-Content-Type-Options中nosniff浏览器会对返回内容做MIME嗅探放大文件上传类攻击Referrer-Policy中same-origin跨域跳转时把URL里的token、sessionId带到第三方站点Permissions-Policy低按业务裁剪页面默认可调摄像头、定位等敏感能力权限收敛靠它为什么用这份而不是OWASP全列表因为X-XSS-Protection已经是个争议项现代浏览器几乎全移除了它保留它的老系统里它本身可能引入新的XSS风险但国内不少政企站点至今在响应里带着这个头的值。把它列入必检项报告里就会混入大量低价值噪音大家看一眼就关掉了真正的高危项反而没人关注。团队用下来把检测项和扫描脚本解耦成字典是最省事的做法。银行项目会往字典里加Clear-Site-Data内容平台加Cross-Origin-Opener-Policy扩展时只改数据不动逻辑。2.2 用Python requests写最小检测器核心代码与参数说明请求头缺失的检测逻辑非常直接发一个请求拿响应头逐个对基线清单。关键是两个细节头不存在要报头存在但值不对也要报。很多系统配了X-Content-Type-Options却把值拼成了noshiff或者HSTS头存在但缺了max-age这类配了等于没配的情况比完全缺失更普遍。import argparse import requests import urllib3 from concurrent.futures import ThreadPoolExecutor, as_completed urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # 头名 - (级别, 必须包含的关键词, 说明) BASE_HEADERS { Strict-Transport-Security: (high, max-age, HSTS未启用或配置不完整), Content-Security-Policy: (high, , 缺少CSPXSS风险上升), X-Frame-Options: (high, , 未禁止被嵌入iframe), X-Content-Type-Options: (medium, nosniff, MIME嗅探未关闭), Referrer-Policy: (medium, , Referer策略缺失), Permissions-Policy: (low, , 浏览器特性权限未收敛), } def check_url(url, timeout10, follow_redirectsFalse): 请求单个URL返回缺失的响应头列表。 timeout 控制单次请求最长等待follow_redirects 控制是否跟随重定向。 try: resp requests.get( url, timeouttimeout, verifyFalse, allow_redirectsfollow_redirects, ) except requests.exceptions.RequestException as e: return {url: url, status: None, error: str(e), missing: []} missing [] for header, (level, keyword, desc) in BASE_HEADERS.items(): value resp.headers.get(header) if value is None: missing.append({header: header, level: level, desc: desc, current: None}) elif keyword and keyword.lower() not in value.lower(): missing.append({header: header, level: level, desc: desc, current: value}) return {url: url, status: resp.status_code, final_url: resp.url, missing: missing}逻辑说明resp.headers.get(header)拿不到头时返回None这比先判断header in resp.headers再取值更简洁少写一层分支。keyword字段是弱校验只确认值里有没有关键片段例如HSTS头必须含max-age才代表真的启用了不写成完整正则是因为各家配置的写法差异很大严格匹配会误伤。resp.url是重定向后的最终地址排障时对照final_url和输入地址是否一致就能立刻看出请求是否被跳到了别的页面。参数说明timeout10对国内网络环境是合理折中太长会让批量扫描卡死太短在慢速链路下会产生大量假超时。verifyFalse是为了处理测试环境普遍存在的自签名证书但生产扫描一定改回verifyTrue否则中间人伪造证书时你的扫描结果会被带偏。allow_redirects设计成参数而不是写死True是为避免登录页污染结果这个坑在第4章会专门展开。提示请求头检测面对的是服务端配置不是业务逻辑对同一个URL发起GET请求基本够用。需要带鉴权Token的接口放到第5章的配置文件里按目标处理。2.3 并发、超时、重定向三个参数的实际调法def scan(urls, workers10, timeout10, follow_redirectsFalse): 并发扫描一批URL失败的不直接报错归入error字段统一展示。 results [] with ThreadPoolExecutor(max_workersworkers) as pool: futures {pool.submit(check_url, u, timeout, follow_redirects): u for u in urls} for fut in as_completed(futures): url futures[fut] try: result fut.result() except Exception as e: result {url: url, status: None, error: f未捕获异常: {e}, missing: []} results.append(result) return results if __name__ __main__: parser argparse.ArgumentParser(descriptionHTTP安全响应头缺失检测工具) parser.add_argument(-u, --url, actionappend, help目标URL可多次传参) parser.add_argument(-f, --file, helpURL列表文件每行一个) parser.add_argument(-w, --workers, typeint, default10) args parser.parse_args() targets list(args.url or []) if args.file: with open(args.file) as fp: targets [line.strip() for line in fp if line.strip()] results scan(targets, workersargs.workers) for r in results: status r.get(status) or ERR for m in r[missing]: print(f[{m[level].upper()}] {r[url]} (HTTP {status}) 缺 {m[header]}: {m[desc]})并发线程数不是越大越好。我在内网扫一个管理后台时试过50并发Nginx直接吐出一批502原因是后端服务的最长响应时间在超时边缘摆动线程全卡在等待上连接被重置后requests抛ConnectionError被当成站点无响应记进报告。把workers降到5、timeout提到15后稳定很多。判断并发是否过高的标准是单URL平均响应时间乘以workers必须明显大于总体扫描时间否则说明请求在排队加大并发只是放大排队效应。HTTP连接复用在这里值得单独说一句requests的Session内部维护连接池能复用同一主机的TCP连接这比每个请求新建连接对Nginx友好得多。可以先跑一遍串行摸清单URL耗时再决定workers往哪个方向调。3. CORS跨域漏洞检测别只看浏览器报错要看配置逻辑3.1 CORS配置错误的几种典型形态CORS检测和请求头缺失检测有一个本质区别请求头缺失是没有CORS漏洞往往是有但错了。服务器返回了Access-Control-Allow-Origin但返回得太宽松。浏览器对跨域读请求的拦截其实是前端兜底配置宽松的接口在攻击者眼里等于拆掉了那道门。开发同学看到跨域报错的第一反应往往是去加一个Access-Control-Allow-Origin: *或者把Origin反射写上去功能通了漏洞也埋下了。几种典型形态先讲清楚。第一种是Origin直接反射。服务端代码写成Access-Control-Allow-Origin: request.getHeader(Origin)来什么回什么。如果再配上Access-Control-Allow-Credentials: true第三方页面可以带着用户Cookie发起跨域读这是CORS漏洞里最严重的一种。第二种是*通配配了Credentials。HTTP规范上*不能和Credentials共存但很多老系统就这么配了浏览器会拒绝这类响应属于配置错误但不直接可利用需要记入整改清单。第三种是预检请求和真实请求行为不一致。Nginx里对OPTIONS统一返回200但不带CORS头的配置非常常见前端联调时暴雷安全扫描时又因为状态码是200被当成通过。第四种是信任子域列表里混入了一个可攻破的子域。比如信任*.example.com但某个子域上偏偏有个反射XSS攻击者就能借那个子域发起跨域请求读主域数据。3.2 自动探测脚本反射Origin、凭证、预检响应一起测import requests import urllib3 urllib3.disable_warnings() # 这里的域名必须和目标完全无关用 localhost 也会产生判定偏差 TEST_ORIGIN https://evil.example.com def inspect_cors(url, timeout10): headers {Origin: TEST_ORIGIN} # 预检请求模拟前端发起带自定义头的跨域请求之前的探测 preflight_headers { Origin: TEST_ORIGIN, Access-Control-Request-Method: GET, Access-Control-Request-Headers: X-Custom-Header, } try: resp requests.get(url, headersheaders, timeouttimeout, verifyFalse) pre_resp requests.options(url, headerspreflight_headers, timeouttimeout, verifyFalse) except requests.exceptions.RequestException as e: return {url: url, error: str(e)} acao resp.headers.get(Access-Control-Allow-Origin) acac resp.headers.get(Access-Control-Allow-Credentials) pre_acao pre_resp.headers.get(Access-Control-Allow-Origin) pre_acac pre_resp.headers.get(Access-Control-Allow-Credentials) issues [] risk info # 判定1真实请求的Origin反射 if acao TEST_ORIGIN: issue 真实请求Origin反射允许任意站读取 if acac and acac.lower() true: risk high issues.append(issue 且携带凭证可跨域读取登录态数据) else: risk medium issues.append(issue 但未携带凭证) elif acao *: issues.append(真实请求使用*通配) if acac and acac.lower() true: issues.append(同时配置Allow-Credentials:true属于无效且危险的组合) risk medium elif acao is not None: issues.append(f返回固定Origin: {acao}需要人工确认该域是否为可信源) risk info # 判定2预检响应是否放行 if pre_acao TEST_ORIGIN: issues.append(预检请求Origin反射) if pre_acac and pre_acac.lower() true: risk high else: risk medium if risk ! high else risk elif pre_acao is None and pre_resp.status_code 200: issues.append(预检请求返回200但未返回Access-Control-Allow-Origin网关放行预检但实际无法读取) risk medium if risk ! high else risk return {url: url, acao: acao, acac: acac, pre_acao: pre_acao, issues: issues, risk: risk}逻辑说明一次检测发两个请求GET带Origin测真实请求OPTIONS带预检头测预检路径。两个响应必须分别判定我在实际项目里见过GET返回配置好的CORS头、OPTIONS却完全不带的组合单独测任何一个都会漏判。预检请求故意带X-Custom-Header这个业务里不常见的自定义头用来验证服务器是不是对Access-Control-Allow-Headers: *全放行——如果连这种未知头都允许说明放行策略宽松到连未知来源的Header都可能被接受这类系统的Origin处理通常也粗糙。参数说明TEST_ORIGIN要选和目标域名完全无关的https域名不要用http://localhost。有的开发环境会放行全部本地地址用localhost测会把测试环境特有的宽松规则错误带到生产结论里。timeout沿用10秒但加了预检请求后每个URL实际耗时翻倍批量扫描时并发数要比请求头检测低一档我一般从10降到5。这个脚本对Access-Control-Request-Headers的取值也可以换成多个头名逗号拼接用来测服务器的白名单匹配是精确匹配还是前缀匹配。3.3 结果分级把配置问题按可利用性排优先级脚本输出的是证据链不是最终结论。分级时把配置问题和可利用漏洞分开风险级判定条件处理建议高危Origin反射 Allow-Credentials:true且接口返回敏感业务字段当天修复改为精确白名单域名中危Origin反射但无凭证评估接口是否公开信息登录后接口仍建议修中危预检返回200但不带allow-origin头排查网关/反向代理的自写CORS过滤器低危固定Origin列表但包含可疑域名定期复核域名归属和证书持有者信息未返回任何CORS头无需处理但确认不是网关层漏配分级逻辑有一个血泪教训见过太多报告把Origin直接反射一律标高危验证时发现接口靠X-Token鉴权而不是Cookie第三方页面即使发得出请求也读不到数据。所以脚本只负责给出证据链反射没反射、带没带凭证、预检放没放行最终风险等级要加上接口是否返回业务敏感字段这个条件。这个条件由人工确认或者交给第6章的浏览器验证去定性。报告里每条CORS发现都应该带上原始头快照方便事后复查而不是再发一遍请求。4. 避坑指南请求头与CORS检测最容易翻车的5个场景下面五条按踩坑频率排序每一条都按现象、原因、解决来讲。这些都是我在真实扫描环境里反复遇到过的不是教科书场景。4.1 现象检测结果全是缺失但浏览器Network面板里明明有现象拿脚本扫了50个URL45个标缺失可这个站点明明是配置过安全头的浏览器里打开Network面板逐条能看到响应头都存在。原因目标URL发生了302跳转到统一登录页脚本跟随重定向后检查的是登录页的响应头。Spring Security这类框架默认只对受保护路径加安全头登录页和静态资源不在保护范围内头自然全缺。更隐蔽的是有的系统跳到了SSO登录网关那批响应头来自另一个服务和你要测的应用完全无关。解决把follow_redirects设成False先看第一跳同时打印final_url凡是最终地址和输入地址路径不一致的先用真实浏览器确认登录流程再决定是带上预登录Cookie重扫还是把该URL排除。带Cookie重扫的做法在配置文件里加headers字段我一般用一个已认证的会话Cookie而不是在代码里硬编码凭据。4.2 现象Origin反射被判高危验证时发现根本带不上Cookie现象CORS检测脚本报了一堆Origin反射携带凭证的高危项写报告前用POC页面逐一验证大部分页面上读到的却是登录页HTML或者未经授权。原因至少三种情况导致假阳性。第一种接口鉴权用的是Authorization头而不是Cookie第三方页面的fetch带不上这个头即使CORS头反射了也读不到业务数据。第二种目标域的Cookie设置了SameSiteLax或SameSiteStrict跨域请求浏览器根本不带Cookie服务端拿不到会话。第三种服务端的反射逻辑对路径有校验只有特定路径才反射脚本测的是根路径而不是真实接口路径得到的结论自然不准。解决脚本把配置高危和可利用高危分开标注。人工复核看三个点接口用Cookie还是Token鉴权、Cookie的SameSite属性、响应体里有没有业务敏感字段比如userId、account这类特征。三个条件都满足才升级为可利用漏洞只满足配置条件的一律写配置待查。4.3 现象预检请求返回200浏览器还是提示has been blocked by CORS policy现象脚本判预检通过因为OPTIONS返回200但浏览器控制台明确报has been blocked by CORS policy: response to preflight request doesnt pass access control check。原因浏览器对预检请求的判定看的是响应头而不是响应码。服务器对OPTIONS统一返回200但不带Access-Control-Allow-Origin头前端联调时暴雷。Nginx配置里还有个著名坑在location块里单独写了add_header会覆盖父级的add_header继承父级声明的CORS头在子location里全部丢失只剩200状态码撑场面。解决脚本判定顺序必须是先看预检响应的pre_acao头是否存在再看状态码。这就是我在代码里写elif pre_acao is None and pre_resp.status_code 200这个分支顺序的原因。报告里把预检200但不带头单独列一个分类和真正的放行区分开方便Nginx配置排查。4.4 现象并发一高全超时报告里冒出一堆假故障现象workers开到30扫描结果里十几条error全是timeout或connection reset单独重扫这些URL又全部正常。原因requests在默认情况下每个请求都新建TCP连接高并发下Nginx的worker_connections被打满连接在等待队列里超时。HTTP连接复用的优势一点没吃到。如果目标站在WAF后面高频请求还会触发拦截策略直接封了扫描出口IP后续所有请求都变成403。解决用requests.Session()共享连接池workers降到5单个URL失败后间隔3秒重试2次。对有明显WAF特征的目标把扫描做成单线程加固定小延迟每秒不超过5个请求慢但稳定。判断是不是被限流的办法很简单看报错的URL是否集中在同一台主机如果是大概率不是目标挂了是连接策略太激进。4.5 现象用无头浏览器验证CORS一切正常换真实浏览器就异常现象用Playwright无头模式跑验证POC控制台没报错数据读到了。可是业务同事在自己电脑上打开同一个页面却提示Permission was denied for this request to a cross-origin resource。原因无头浏览器和真实浏览器在Cookie策略、扩展注入、TLS指纹上的行为差异本来就是玄学。但更常见的坑是验证代码里漏了credentials: include默认跨域请求不带CookieCORS配置再宽松也读不到数据自然一切正常。解决把credentials: include写死在POC里验证环境优先用有头模式起浏览器。结论性验证一律以真实浏览器的console输出为准无头模式只用来批量初筛。报告里标注验证方式是真实浏览器还是有头模式避免复核人和你看到的结果不一样。5. 把检测工具接进日常流程配置管理、报告输出与CI接入5.1 用配置文件管理扫描目标而不是堆命令行参数命令行传URL只适合临时验证一把。团队用的时候目标列表是动态的接口要带Token才能返回真实响应头所以配置文件才是正确的做法。核心结构是一个targets数组每项包含url、method和headers全局options放timeout、workers、follow_redirects。{ targets: [ {url: https://api.example.com/v1/users, method: GET, headers: {Authorization: Bearer ${API_TOKEN}}}, {url: https://www.example.com/login, method: GET, headers: {}}, {url: https://admin.example.com/export, method: GET, headers: {X-Api-Key: abc123}} ], options: {timeout: 10, workers: 5, follow_redirects: false} }import json import os def load_config(path): 读取配置文件并把${ENV_VAR}形式的占位符替换为真实环境变量。 with open(path, encodingutf-8) as fp: cfg json.load(fp) # 递归替换占位符避免硬编码Token进仓库 def replace_env(obj): if isinstance(obj, str): if obj.startswith(${) and obj.endswith(}): return os.environ.get(obj[2:-1], ) return obj elif isinstance(obj, dict): return {k: replace_env(v) for k, v in obj.items()} elif isinstance(obj, list): return [replace_env(item) for item in obj] return obj return replace_env(cfg)逻辑说明item.get(timeout, options.get(timeout, 10))这种逐级覆盖是实现上最省心的做法——某个接口响应慢就单独给它配20秒不用全站跟着调大超时。配置文件里的${API_TOKEN}占位符在加载时替换成环境变量避免Token以明文形式进入扫描报告和Git提交记录。选JSON而不是YAML是因为Python标准库直接支持团队里任何成员都能看懂改对少一个解析依赖就少一层出错可能。5.2 输出JSON报告并用退出码打通CItxt报告适合人读CI和后续分析要的是结构化数据。输出格式固定为generated_at、total、high、medium、details五个字段details里每条记录带url、risk、issue类型和原始响应头快照。import json import time import sys def write_report(results, outputscan_report.json): 写JSON报告返回高危数量供上层决定退出码。 payload { generated_at: time.strftime(%Y-%m-%dT%H:%M:%S%z), total: len(results), high: sum(1 for r in results if r.get(risk) high), medium: sum(1 for r in results if r.get(risk) medium), details: results, } with open(output, w, encodingutf-8) as fp: json.dump(payload, fp, ensure_asciiFalse, indent2) return payload[high] # 调用示例高危超过阈值则非0退出 high_count write_report(results) sys.exit(1 if high_count 0 else 0)逻辑说明把high数量作为函数返回值由上层根据团队定的阈值决定退出码——有的团队接受0高危有的接受10以内阈值写成配置项比写死在工具里更灵活。ensure_asciiFalse让报告里的中文可读generated_at用带时区的ISO 8601格式跨时区协作时对比报告不会错位。响应头快照字段对排障很有价值报告里看到缺失项时可以直接翻当时的完整头不用再跑到环境里复测一遍。5.3 CI里跑这个工具的3个注意点如果直接把整个工具塞进merge request的流水线大概率被队友骂。三个注意点保平安。第一只扫本次改动涉及的路由。全站扫描放在每晚定时任务里MR流水线里按路由前缀过滤出变更接口把单次扫描时间压到1分钟以内。定时任务负责兜底MR扫描负责回归两者职责分开。第二网络隔离。扫描容器能访问测试环境不能访问生产环境。用NetworkPolicy或代理参数双重限制防止配置错误把生产域名扫了——我见过一次把example.com写成test.example.com导致生产接口被打出告警的事故。第三报告比对。建一个基线目录每周对比一次基线新增的高危项自动开issue消失的高危项自动关issue。只有存在对照组扫描才叫回归验证单次扫描报告能证明的意义只占一半。6. 进阶验证CORS漏洞是否真实可利用用浏览器行为给配置问题定性前面的脚本负责定性这个URL有没有CORS配置问题。真正写漏洞报告前还差一步——证明攻击者真的能从第三方页面读到数据。这一步用真实浏览器验证最可靠。做法是起一个本地静态页面放一段fetch代码目标地址填要验证的接口credentials: include必须写。!DOCTYPE html html body pre idresult等待请求…/pre script fetch(https://target.example.com/api/user/info, { method: GET, credentials: include }).then(r r.text()).then(text { document.getElementById(result).innerText 读取成功:\n text; }).catch(err { document.getElementById(result).innerText 已被拦截:\n err.message; }); /script /body /html验证时看三个结果直接显示JSON数据证明跨域读可行漏洞成立显示已被拦截说明虽然响应头配错但浏览器兜住了配置问题降级为中危显示的是登录页HTML而不是JSON说明接口需要特定会话态CORS问题先记入整改清单。我自己现在的习惯是工具扫出来的CORS问题一律用这个页面过一遍再写报告没做过这一步的直接标配置待查。请求头缺失那边不用这么费事——缺失就是缺失没有可利用性论证的过程。这套流程用下来最明显的改变是漏洞报告里的废话变少以前那种建议启用CSP之类的万能建议不再刷屏每条结论都有人愿意认真看完希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑