CORS(跨域资源共享)是浏览器同源策略的重要补充机制,但它也一直是API安全漏洞的高发地带。很多接口为了图省事,直接把请求方携带的Origin原样反射回Access-Control-Allow-Origin响应头,甚至配合Access-Control-Allow-Credentials设为true,这就给了恶意网站跨域读取用户敏感数据的机会。与其等渗透测试报告指出问题,不如自己动手用Node.js写一个自动化检测脚本,把CORS配置错误纳入持续集成流程,每次接口变更都自动扫描一遍。

先搞清楚CORS配置错误的几种典型模式
写检测工具之前,必须先明确要检测什么。CORS配置错误并不是一个单一问题,而是一组症状相似但危害程度不同的错误集合。第一种是Origin反射,服务端不校验来源,把请求头里的Origin直接塞回响应头。这种情况下任何网站都能以任意身份读取该接口的响应内容。第二种是通配符滥用,即返回Access-Control-Allow-Origin: *,单独使用通配符本身危害有限,因为浏览器规范禁止通配符与凭证模式同时生效,但如果接口同时存在敏感操作且无其他鉴权,依然构成风险。
第三种是null Origin放行。某些配置允许Origin为null的请求通过,而null Origin可以通过沙盒iframe、本地文件页面等方式伪造,攻击成本很低。第四种是子域名绕过,比如用字符串包含判断来校验Origin,攻击者注册一个类似evil.com的域名,或者利用目标域下的开放重定向,就可能骗过校验逻辑。此外还有凭证放行问题:当Access-Control-Allow-Credentials为true时,浏览器会自动带上Cookie,一旦Origin校验形同虚设,攻击者就能以受害者身份调用接口。
理解这些模式后,检测逻辑就清晰了:构造不同性质的Origin去请求目标接口,观察响应头的变化,根据响应头组合判断漏洞类型。这正好适合用脚本自动化完成。
用Node.js实现核心检测逻辑
检测脚本的核心是HTTP请求与响应头分析。Node.js自带的fetch(Node 18以上版本内置)已经足够好用,不需要额外依赖。下面是一个完整的基础检测模块:
const VULN = {
REFLECT: 'Origin反射漏洞',
NULL_ORIGIN: 'null Origin放行',
WILDCARD: '通配符配置',
CREDS: '凭证放行风险'
};
// 发送携带指定Origin的请求并解析响应头
async function probe(target, origin) {
const headers = origin === null ? {} : { Origin: origin };
try {
const res = await fetch(target, { headers, redirect: 'manual' });
return {
status: res.status,
acao: res.headers.get('access-control-allow-origin'),
acac: res.headers.get('access-control-allow-credentials')
};
} catch (e) {
return { status: 0, error: e.message };
}
}
// 对单个目标执行完整检测
async function scanTarget(target) {
const findings = [];
const evilOrigin = 'https://evil.ipipp.com';
// 1. 检测Origin反射
const reflect = await probe(target, evilOrigin);
if (reflect.acao === evilOrigin) {
findings.push({ type: VULN.REFLECT, detail: '任意Origin被反射回ACAO头' });
if (reflect.acac === 'true') {
findings.push({ type: VULN.CREDS, detail: '反射Origin且允许凭证,可携带Cookie跨域读取' });
}
}
// 2. 检测null Origin
const nullProbe = await probe(target, null);
// fetch无法直接发送Origin: null,这里用原始http模块补充
if (nullProbe.acao === '*') {
findings.push({ type: VULN.WILDCARD, detail: '响应返回通配符' });
}
return { target, findings };
}上面这段代码有几个细节值得展开。首先是Origin为null的构造方式,fetch的headers对象无法显式设置Origin为null字符串(部分实现会忽略),更可靠的做法是使用Node原生的http模块直接写入原始头字段。其次是redirect处理,一定要设置为manual,否则fetch会自动跟随重定向,响应头就变成了最终落地页的头,可能掩盖真实配置。
补充null Origin检测的原始请求实现如下:
const http = require('http');
const https = require('https');
const { URL } = require('url');
function rawProbe(target, originValue) {
return new Promise((resolve, reject) => {
const u = new URL(target);
const lib = u.protocol === 'https:' ? https : http;
const req = lib.request({
hostname: u.hostname,
port: u.port,
path: u.pathname + u.search,
method: 'GET',
headers: originValue ? { Origin: originValue } : {}
}, res => {
resolve({
status: res.statusCode,
acao: res.headers['access-control-allow-origin'],
acac: res.headers['access-control-allow-credentials']
});
res.resume(); // 释放响应体
});
req.on('error', reject);
req.end();
});
}
// 使用示例:发送Origin为字符串null的请求
async function checkNullOrigin(target) {
const r = await rawProbe(target, 'null');
return r.acao === 'null';
}扩展为批量扫描与风险报告工具
单个接口的检测只是起点,实际项目中需要扫描整个API面。可以准备一个接口清单文件,配合并发控制批量执行,最后汇总输出结构化报告。这里用p-limit风格的简单并发池即可,避免一次性发出过多请求压垮目标服务:
const fs = require('fs');
async function runPool(tasks, limit) {
const results = [];
let index = 0;
async function worker() {
while (index < tasks.length) {
const i = index++;
results[i] = await tasks[i]();
}
}
await Promise.all(Array.from({ length: limit }, worker));
return results;
}
async function main() {
// 从JSON文件读取目标列表
const targets = JSON.parse(fs.readFileSync('targets.json', 'utf8'));
const tasks = targets.map(t => () => scanTarget(t).catch(e => ({ t, error: e.message })));
const report = await runPool(tasks, 5);
console.log('===== CORS安全扫描报告 =====');
for (const item of report) {
if (!item.findings || item.findings.length === 0) {
console.log(`[安全] ${item.target}`);
} else {
console.log(`[风险] ${item.target}`);
item.findings.forEach(f => console.log(` - ${f.type}: ${f.detail}`));
}
}
// 输出JSON供CI系统消费
fs.writeFileSync('cors-report.json', JSON.stringify(report, null, 2));
}
main();关于误报控制,有几点实战经验。一是反射判断要精确匹配,不要用includes去比较ACAO头,否则evil.ippipp.com的反射可能被误判为ippipp.com的合法配置。二是关注预检请求,部分服务只在OPTIONS预检中返回正确的CORS头,而实际GET请求的头是错的,反之亦然,严谨的检测应该两种都发。三是区分环境,测试环境允许的Origin集合和生产环境往往不同,扫描时要用目标环境真实的域名做白名单比对。
最后把这个脚本接入CI流程很简单:在流水线中执行node scan.js,检测到高风险发现(尤其是反射加凭证组合)时返回非零退出码,让构建失败并阻止有漏洞的配置上线。再配合定时任务对已上线接口做周期性巡检,就能形成一套完整的CORS配置持续监控机制,把这类隐蔽但致命的API安全风险牢牢管住。