security: CORS 收敛为白名单 + 基础安全响应头 + 隐藏技术栈
- CORS 默认仅同源可用,跨域需显式配置 CORS_ORIGINS(非生产默认放行 localhost:5173) - 新增安全响应头:X-Content-Type-Options / X-Frame-Options / Referrer-Policy / X-XSS-Protection / Permissions-Policy;生产附加 HSTS - 关闭 X-Powered-By,设置 trust proxy - 文档补充 CORS_ORIGINS;新增回归护栏 cors-security-headers.spec.ts(85 套件/627 用例全绿)
这个提交包含在:
@@ -34,7 +34,46 @@ import { setWeChatRefundFn } from './core/pay/refund-bridge';
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors());
|
||||
// 反向代理(Nginx)后正确识别客户端 IP(登录限流等使用)
|
||||
app.set('trust proxy', 1);
|
||||
// 隐藏技术栈标识
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// CORS 收敛:默认仅同源可用;如需跨域,配置 CORS_ORIGINS(逗号分隔)显式放行
|
||||
const configuredCorsOrigins = String(process.env.CORS_ORIGINS || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const allowedCorsOrigins = new Set(configuredCorsOrigins);
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// 本地开发默认允许 Vite dev server 直连 API(若未使用代理)
|
||||
allowedCorsOrigins.add('http://localhost:5173');
|
||||
allowedCorsOrigins.add('http://127.0.0.1:5173');
|
||||
allowedCorsOrigins.add('http://localhost:3000');
|
||||
}
|
||||
app.use(
|
||||
cors({
|
||||
origin(origin, cb) {
|
||||
// 非浏览器请求(无 Origin,如小程序/服务端)放行
|
||||
if (!origin) return cb(null, true);
|
||||
return cb(null, allowedCorsOrigins.has(origin));
|
||||
},
|
||||
credentials: false,
|
||||
})
|
||||
);
|
||||
|
||||
// 基础安全响应头
|
||||
app.use((_req: Request, res: Response, next: NextFunction) => {
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
res.setHeader('Referrer-Policy', 'no-referrer');
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
res.setHeader('Strict-Transport-Security', 'max-age=15552000; includeSubDomains');
|
||||
}
|
||||
next();
|
||||
});
|
||||
app.use(
|
||||
express.json({
|
||||
// 保留原始 HTTP 报文,供微信支付 V3 回调验签使用
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第六十九批回归闸门:CORS 收敛 + 基础安全响应头
|
||||
*/
|
||||
describe('第六十九批 CORS 与安全响应头闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
|
||||
it('CORS 不再全开,改为白名单(CORS_ORIGINS)', () => {
|
||||
expect(indexCode).not.toContain('app.use(cors());');
|
||||
expect(indexCode).toContain('CORS_ORIGINS');
|
||||
expect(indexCode).toContain('allowedCorsOrigins.has(origin)');
|
||||
});
|
||||
|
||||
it('设置基础安全响应头并隐藏技术栈', () => {
|
||||
expect(indexCode).toContain("app.disable('x-powered-by')");
|
||||
expect(indexCode).toContain("'X-Content-Type-Options', 'nosniff'");
|
||||
expect(indexCode).toContain("'X-Frame-Options', 'DENY'");
|
||||
expect(indexCode).toContain("'Referrer-Policy', 'no-referrer'");
|
||||
expect(indexCode).toContain('Strict-Transport-Security');
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户