fix(security): CORS 不再反射任意 Origin; X-Forwarded-For 改走 trust proxy
1. cors.ts: 不再把任意请求 Origin 原样反射为 Access-Control-Allow-Origin
(配合 credentials:true = 任意站点可携带 Cookie 跨域调用管理 API)。
新策略: 仅放行 a) 无 Origin 头(同源/直连) b) Origin host 与请求 host 一致
(本机多端口开发) c) webui.json allowOrigins 精确白名单。
新增 Vary: Origin。
2. IP 访问控制: 客户端 IP 不再读取客户端可伪造的 X-Forwarded-For 头,
统一走 req.ip(express 'trust proxy' 控制可信代理)。enableXForwardedFor
字段保留(向后兼容)但不再影响 IP 判定, 需要代理真实 IP 的场景应配置
app.set('trust proxy')。
3. WebUiConfigSchema 新增 allowOrigins: string[] (default []), 与前端
GetConfig/UpdateConfig 同步支持。
回归注意: 若部署依赖旧反射行为的前端跨域, 需在 webui.json 配 allowOrigins。
This commit is contained in:
1 parent
6f9d3cdb0f
commit
fe5dafc527
2 files changed
+20
-23
No files matched your search
@@ -32,7 +32,10 @@ const WebUiConfigSchema = Type.Object({
|
||||
// IP黑名单列表
|
||||
ipBlacklist: Type.Array(Type.String(), { default: [] }),
|
||||
// 是否启用 X-Forwarded-For 获取真实IP
|
||||
// 安全提示:该请求头可被客户端伪造,仅在部署于可信反向代理(会重写该头)之后才有意义。
|
||||
enableXForwardedFor: Type.Boolean({ default: false }),
|
||||
// 允许跨域访问的前端源列表(精确匹配,含协议)。空列表 = 仅允许同源/本机 host 一致的 Origin。
|
||||
allowOrigins: Type.Array(Type.String(), { default: [] }),
|
||||
// 是否启用双因素认证(2FA)
|
||||
enable2FA: Type.Boolean({ default: false }),
|
||||
// TOTP 密钥
|
||||
|
||||
@@ -184,24 +184,9 @@ function isIPInList (ip: string, ipList: string[]): boolean {
|
||||
export const cors: RequestHandler = async (req, res, next) => {
|
||||
const config = await WebUiConfig.GetWebUIConfig();
|
||||
|
||||
// 根据配置决定如何获取客户端IP
|
||||
let clientIP: string;
|
||||
if (config.enableXForwardedFor) {
|
||||
// 启用 X-Forwarded-For 时,优先从该头部获取真实IP
|
||||
const forwardedFor = req.headers['x-forwarded-for'];
|
||||
if (typeof forwardedFor === 'string') {
|
||||
// X-Forwarded-For 可能包含多个IP,取第一个
|
||||
clientIP = forwardedFor.split(',')[0]?.trim() || '';
|
||||
} else if (Array.isArray(forwardedFor) && forwardedFor.length > 0) {
|
||||
clientIP = forwardedFor[0] || '';
|
||||
} else {
|
||||
// 如果没有 X-Forwarded-For,回退到 socket IP
|
||||
clientIP = req.ip || req.socket.remoteAddress || '';
|
||||
}
|
||||
} else {
|
||||
// 默认使用 socket IP
|
||||
clientIP = req.ip || req.socket.remoteAddress || '';
|
||||
}
|
||||
// 获取客户端IP优先走 req.ip(express 'trust proxy' 负责可信代理的解析),
|
||||
// 不再无条件信任请求方可伪造的 X-Forwarded-For 头。
|
||||
const clientIP: string = req.ip || req.socket.remoteAddress || '';
|
||||
|
||||
// 检查访问控制模式
|
||||
if (config.accessControlMode === 'whitelist') {
|
||||
@@ -219,11 +204,20 @@ export const cors: RequestHandler = async (req, res, next) => {
|
||||
}
|
||||
// 'none' 模式:不进行任何限制
|
||||
|
||||
const origin = req.headers.origin || '*';
|
||||
res.header('Access-Control-Allow-Origin', origin);
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
||||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
// CORS:不再反射任意 Origin。
|
||||
// 未配置 allowOrigins 时,只有「无 Origin 头(同源/直连工具)」或「Origin 主机与请求 host 一致(本地多端口开发)」才放行跨域头,
|
||||
// 其余一律不带 ACAO 头(浏览器会拒绝跨域响应)。
|
||||
const origin = req.headers.origin;
|
||||
const requestHost = req.headers.host || '';
|
||||
const originHost = typeof origin === 'string' && origin.startsWith('http') ? origin.replace(/^https?:\/\//, '').split('/')[0] ?? '' : '';
|
||||
const hostMatches = originHost !== '' && originHost === requestHost;
|
||||
if (origin && (config.allowOrigins?.length ? config.allowOrigins.includes(origin) : hostMatches)) {
|
||||
res.header('Access-Control-Allow-Origin', origin);
|
||||
res.header('Vary', 'Origin');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
||||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.sendStatus(204);
|
||||
|
||||
Reference in new issue
Block a user