diff --git a/app/api/login/auth.server.ts b/app/api/login/auth.server.ts index 6ba7a6f..603a652 100644 --- a/app/api/login/auth.server.ts +++ b/app/api/login/auth.server.ts @@ -736,6 +736,18 @@ export async function simpleRootLogin( }); } + // 密码强度验证 + // const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/; + // if (!passwordRegex.test(password.trim())) { + // return new Response(JSON.stringify({ + // success: false, + // error: "密码必须至少8位,包含大小写字母和数字" + // }), { + // status: 400, + // headers: { "Content-Type": "application/json" } + // }); + // } + // 调用登录接口 const loginResponse = await fetch(`${API_BASE_URL}/password_login`, { method: 'POST', diff --git a/app/config/api-config.ts b/app/config/api-config.ts index 66e18ca..965f722 100644 --- a/app/config/api-config.ts +++ b/app/config/api-config.ts @@ -340,7 +340,10 @@ const getCurrentConfig = (): ApiConfig => { return getConfigFromEnv(defaultConfig); } - console.log('🔧 最终配置:', defaultConfig); + // 调试信息(仅在开发环境显示) + if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { + console.log('🔧 最终配置:', defaultConfig); + } return defaultConfig; }; diff --git a/app/middleware/host-validation.ts b/app/middleware/host-validation.ts new file mode 100644 index 0000000..6d0f242 --- /dev/null +++ b/app/middleware/host-validation.ts @@ -0,0 +1,238 @@ +/** + * Host头验证中间件 + * 用于防止Host Header注入攻击 + */ + +interface HostValidationResult { + valid: boolean; + error?: string; + allowedHost?: string; +} + +/** + * 获取允许的Host列表 + * 根据不同环境和端口配置返回相应的Host白名单 + */ +function getAllowedHosts(): string[] { + // 基础IP地址 + const baseIP = '10.79.97.17'; + const testIP = '172.16.0.55'; + const localhostIP = '127.0.0.1'; + + // 生产环境端口列表 + const productionPorts = ['51703', '51704', '51705', '51706', '51707', '51708']; + + // 测试环境端口列表 + const testPorts = ['5173', '5174', '5175', '5176', '5177', '5178']; + + const allowedHosts: string[] = []; + + // 添加基础IP(不带端口) + allowedHosts.push(baseIP, testIP, 'localhost', localhostIP); + + // 添加生产环境的IP:端口组合 + productionPorts.forEach(port => { + allowedHosts.push(`${baseIP}:${port}`); + }); + + // 添加测试环境的IP:端口组合 + testPorts.forEach(port => { + allowedHosts.push(`${testIP}:${port}`); + allowedHosts.push(`localhost:${port}`); + allowedHosts.push(`${localhostIP}:${port}`); + }); + + // 开发环境额外允许的Host + if (process.env.NODE_ENV === 'development') { + allowedHosts.push('localhost:3000', '127.0.0.1:3000'); + } + + return allowedHosts; +} + +/** + * 验证Host头是否在允许列表中 + * @param request - Remix Request对象 + * @returns 验证结果 + */ +export function validateHost(request: Request): HostValidationResult { + const host = request.headers.get('host'); + const referer = request.headers.get('referer'); + const userAgent = request.headers.get('user-agent'); + + // 获取允许的Host列表 + const allowedHosts = getAllowedHosts(); + + console.log('🔒 Host验证开始:', { + host: host, + referer: referer, + userAgent: userAgent ? userAgent.substring(0, 50) + '...' : null, + allowedHosts: allowedHosts + }); + + // 1. 验证Host头是否存在 + if (!host) { + console.error('❌ Host头缺失'); + return { + valid: false, + error: 'Missing Host header' + }; + } + + // 2. 验证Host头是否在允许列表中 + if (!allowedHosts.includes(host)) { + console.error('❌ Host头不在允许列表中:', { + host: host, + allowedHosts: allowedHosts + }); + return { + valid: false, + error: `Invalid Host header: ${host}` + }; + } + + // 3. 验证Referer头(如果存在) + if (referer) { + try { + const refererUrl = new URL(referer); + const refererHost = refererUrl.host; + + if (!allowedHosts.includes(refererHost)) { + console.error('❌ Referer头不在允许列表中:', { + referer: referer, + refererHost: refererHost, + allowedHosts: allowedHosts + }); + return { + valid: false, + error: `Invalid Referer header: ${refererHost}` + }; + } + } catch (error) { + console.error('❌ Referer头格式无效:', referer); + return { + valid: false, + error: `Malformed Referer header: ${referer}` + }; + } + } + + console.log('✅ Host验证通过:', host); + return { + valid: true, + allowedHost: host + }; +} + +/** + * 验证Origin头是否在允许列表中 + * @param request - Remix Request对象 + * @returns 验证结果 + */ +export function validateOrigin(request: Request): HostValidationResult { + const origin = request.headers.get('origin'); + + if (!origin) { + // Origin头不是必须的,某些请求(如直接访问)可能没有Origin头 + return { valid: true }; + } + + try { + const originUrl = new URL(origin); + const originHost = originUrl.host; + const allowedHosts = getAllowedHosts(); + + if (!allowedHosts.includes(originHost)) { + console.error('❌ Origin头不在允许列表中:', { + origin: origin, + originHost: originHost, + allowedHosts: allowedHosts + }); + return { + valid: false, + error: `Invalid Origin header: ${originHost}` + }; + } + + console.log('✅ Origin验证通过:', origin); + return { valid: true }; + } catch (error) { + console.error('❌ Origin头格式无效:', origin); + return { + valid: false, + error: `Malformed Origin header: ${origin}` + }; + } +} + +/** + * 完整的请求验证 + * 包括Host、Referer、Origin头的验证 + * @param request - Remix Request对象 + * @returns 验证结果 + */ +export function validateRequest(request: Request): HostValidationResult { + // 1. 验证Host头 + const hostValidation = validateHost(request); + if (!hostValidation.valid) { + return hostValidation; + } + + // 2. 验证Origin头 + const originValidation = validateOrigin(request); + if (!originValidation.valid) { + return originValidation; + } + + return { valid: true }; +} + +/** + * 检查是否为受保护的路由 + * 某些路由可能需要更严格的验证 + * @param pathname - 请求路径 + * @returns 是否为受保护路由 + */ +export function isProtectedRoute(pathname: string): boolean { + const protectedRoutes = [ + '/callback', + '/api/oauth/token', + '/api/oauth/userinfo', + '/logout', + '/admin' + ]; + + return protectedRoutes.some(route => pathname.startsWith(route)); +} + +/** + * 记录安全事件 + * @param event - 事件类型 + * @param details - 事件详情 + * @param request - 请求对象 + */ +export function logSecurityEvent( + event: 'host_validation_failed' | 'origin_validation_failed' | 'referer_validation_failed', + details: string, + request: Request +) { + const timestamp = new Date().toISOString(); + const url = new URL(request.url); + const clientIP = request.headers.get('x-forwarded-for') || + request.headers.get('x-real-ip') || + 'unknown'; + + console.error(`🚨 安全事件: ${event}`, { + timestamp: timestamp, + url: url.pathname + url.search, + host: request.headers.get('host'), + referer: request.headers.get('referer'), + origin: request.headers.get('origin'), + userAgent: request.headers.get('user-agent'), + clientIP: clientIP, + details: details + }); + + // TODO: 这里可以添加更复杂的日志记录逻辑 + // 比如写入数据库、发送告警邮件等 +} diff --git a/app/root.tsx b/app/root.tsx index 69f81fd..1c3b379 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -38,6 +38,11 @@ import { logout, type UserRole } from "~/api/login/auth.server"; +import { + validateRequest, + isProtectedRoute, + logSecurityEvent +} from "~/middleware/host-validation"; // 定义需要高级权限的路径 export const developerOnlyPaths = [ @@ -70,6 +75,32 @@ export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const pathname = url.pathname; + // ==================== Host头验证 ==================== + // 1. 首先进行Host头验证,防止Host Header注入攻击 + const hostValidation = validateRequest(request); + if (!hostValidation.valid) { + // 记录安全事件 + logSecurityEvent('host_validation_failed', hostValidation.error || 'Unknown validation error', request); + + // 对于受保护的路由,直接返回403错误 + if (isProtectedRoute(pathname)) { + throw new Response("Forbidden: Invalid Host header", { + status: 403, + statusText: "Forbidden" + }); + } + + // 对于普通路由,重定向到错误页面 + console.error('❌ Host验证失败:', hostValidation.error); + throw new Response("Forbidden: Invalid request headers", { + status: 403, + statusText: "Forbidden" + }); + } + + // console.log('✅ Host验证通过,继续处理请求'); + // ==================== Host头验证结束 ==================== + // 排除不需要登录验证的路径 const publicPaths = ['/login', '/favicon.ico', '/callback']; const isPublicPath = publicPaths.some(path => pathname.startsWith(path)); diff --git a/app/routes/api.oauth.token.tsx b/app/routes/api.oauth.token.tsx index 50e540d..95f5a8b 100644 --- a/app/routes/api.oauth.token.tsx +++ b/app/routes/api.oauth.token.tsx @@ -1,6 +1,7 @@ import { type ActionFunctionArgs, json } from "@remix-run/node"; import { OAuthClient } from "~/api/login/oauth-client"; import { OAUTH_CONFIG } from "~/config/api-config"; +import { validateRequest, logSecurityEvent } from "~/middleware/host-validation"; /** * 这个Action作为获取OAuth Access Token的服务器端代理。 @@ -8,7 +9,15 @@ import { OAUTH_CONFIG } from "~/config/api-config"; * 以避免在网络策略限制服务器直接访问外部服务时出现问题。 */ export async function action({ request }: ActionFunctionArgs) { - // 1. 只允许POST请求 + // 1. Host头验证 + const hostValidation = validateRequest(request); + if (!hostValidation.valid) { + logSecurityEvent('host_validation_failed', hostValidation.error || 'Unknown validation error', request); + console.error('❌ OAuth Token API Host验证失败:', hostValidation.error); + return json({ success: false, error: "Forbidden: Invalid Host header" }, { status: 403 }); + } + + // 2. 只允许POST请求 if (request.method !== "POST") { return json({ success: false, error: "Method Not Allowed" }, { status: 405 }); } diff --git a/app/routes/api.oauth.userinfo.tsx b/app/routes/api.oauth.userinfo.tsx index a2c81ec..beec8d5 100644 --- a/app/routes/api.oauth.userinfo.tsx +++ b/app/routes/api.oauth.userinfo.tsx @@ -1,12 +1,22 @@ import { type ActionFunctionArgs, json } from "@remix-run/node"; import { OAuthClient } from "~/api/login/oauth-client"; import { OAUTH_CONFIG } from "~/config/api-config"; +import { validateRequest, logSecurityEvent } from "~/middleware/host-validation"; /** * 这个Action作为获取用户信息的服务器端代理。 * 它接收来自前端的`access_token`,然后在后端安全地获取用户信息。 */ export async function action({ request }: ActionFunctionArgs) { + // 1. Host头验证 + const hostValidation = validateRequest(request); + if (!hostValidation.valid) { + logSecurityEvent('host_validation_failed', hostValidation.error || 'Unknown validation error', request); + console.error('❌ OAuth UserInfo API Host验证失败:', hostValidation.error); + return json({ success: false, error: "Forbidden: Invalid Host header" }, { status: 403 }); + } + + // 2. 只允许POST请求 if (request.method !== "POST") { return json({ success: false, error: "Method Not Allowed" }, { status: 405 }); } diff --git a/app/routes/callback.tsx b/app/routes/callback.tsx index ca842ae..165d1ee 100644 --- a/app/routes/callback.tsx +++ b/app/routes/callback.tsx @@ -1,8 +1,23 @@ import { type LoaderFunctionArgs, redirect } from "@remix-run/node"; -import { createUserSession, saveUserInfo } from "~/api/login/auth.server"; +import { createUserSession, saveUserInfo, type UserRole } from "~/api/login/auth.server"; import { JWTUtils, type UserInfoForJWT } from "~/utils/jwt"; +import { validateRequest, logSecurityEvent } from "~/middleware/host-validation"; export async function loader({ request }: LoaderFunctionArgs) { + // ==================== Host头验证 ==================== + // OAuth回调是安全敏感的操作,需要严格验证请求来源 + const hostValidation = validateRequest(request); + if (!hostValidation.valid) { + // 记录安全事件 + logSecurityEvent('host_validation_failed', hostValidation.error || 'Unknown validation error', request); + + console.error('❌ OAuth回调Host验证失败:', hostValidation.error); + return redirect("/login?error=invalid_host"); + } + + // console.log('✅ OAuth回调Host验证通过'); + // ==================== Host头验证结束 ==================== + const url = new URL(request.url); const origin = url.origin; // 获取请求的源 (e.g., "http://10.79.97.17:51703") const code = url.searchParams.get("code"); @@ -143,7 +158,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // 使用统一的session创建函数 return createUserSession({ isAuthenticated: true, - userRole: userRole as 'common' | 'developer', + userRole: userRole as UserRole, redirectTo, accessToken: tokenResponse.access_token, refreshToken: tokenResponse.refresh_token,