diff --git a/server/utils/inputValidator.cjs b/server/utils/inputValidator.cjs index 92f2c4c..7fbe479 100644 --- a/server/utils/inputValidator.cjs +++ b/server/utils/inputValidator.cjs @@ -179,7 +179,7 @@ class InputValidator { this.validatePattern(birthDate, this.validationRules.date, '出生日期'); // 验证日期有效性 - const date = new Date(birthDate); + const date = new Date(birthDate + 'T00:00:00.000Z'); if (isNaN(date.getTime())) { throw new AppError( this.formatErrorMessage('invalid_date', { field: '出生日期' }), @@ -188,24 +188,52 @@ class InputValidator { ); } - // 验证日期范围(1900-2100) + // 验证日期范围(1800-2100)- 扩大范围支持更多历史日期 const year = date.getFullYear(); - if (year < 1900 || year > 2100) { + if (year < 1800 || year > 2100) { throw new AppError( - '出生日期年份应在1900-2100年之间', + '出生日期年份应在1800-2100年之间', 400, 'VALIDATION_ERROR' ); } - // 验证不能是未来日期 - if (date > new Date()) { + // 验证不能是未来日期(允许今天) + const today = new Date(); + today.setHours(23, 59, 59, 999); // 设置为今天的最后一刻 + if (date > today) { throw new AppError( '出生日期不能是未来日期', 400, 'VALIDATION_ERROR' ); } + + // 验证月份和日期的合理性 + const [yearStr, monthStr, dayStr] = birthDate.split('-'); + const month = parseInt(monthStr, 10); + const day = parseInt(dayStr, 10); + + if (month < 1 || month > 12) { + throw new AppError( + '月份应在1-12之间', + 400, + 'VALIDATION_ERROR' + ); + } + + // 验证每月的天数 + const daysInMonth = new Date(year, month, 0).getDate(); + if (day < 1 || day > daysInMonth) { + throw new AppError( + `${year}年${month}月只有${daysInMonth}天`, + 400, + 'VALIDATION_ERROR' + ); + } + + // 验证特殊日期(如闰年) + this.validateSpecialDates(birthDate); } /** @@ -442,6 +470,156 @@ class InputValidator { return message; } + /** + * 验证特殊日期(如闰年2月29日) + * @param {string} birthDate 出生日期 + * @throws {AppError} 验证失败时抛出错误 + */ + validateSpecialDates(birthDate) { + const [yearStr, monthStr, dayStr] = birthDate.split('-'); + const year = parseInt(yearStr, 10); + const month = parseInt(monthStr, 10); + const day = parseInt(dayStr, 10); + + // 验证闰年2月29日 + if (month === 2 && day === 29) { + const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); + if (!isLeapYear) { + throw new AppError( + `${year}年不是闰年,2月没有29日`, + 400, + 'VALIDATION_ERROR' + ); + } + } + } + + /** + * 验证时区信息 + * @param {string} timezone 时区 + * @throws {AppError} 验证失败时抛出错误 + */ + validateTimezone(timezone) { + if (!timezone) return; + + const validTimezones = [ + 'Asia/Shanghai', 'Asia/Hong_Kong', 'Asia/Taipei', 'Asia/Tokyo', + 'America/New_York', 'America/Los_Angeles', 'Europe/London', + 'UTC', 'GMT', 'CST', 'EST', 'PST' + ]; + + // 支持UTC偏移格式 (+08:00, -05:00等) + const utcOffsetPattern = /^[+-]\d{2}:\d{2}$/; + + if (!validTimezones.includes(timezone) && !utcOffsetPattern.test(timezone)) { + throw new AppError( + '时区格式不正确,请使用标准时区名称或UTC偏移格式', + 400, + 'VALIDATION_ERROR' + ); + } + } + + /** + * 验证IP地址 + * @param {string} ip IP地址 + * @throws {AppError} 验证失败时抛出错误 + */ + validateIP(ip) { + if (!ip) return; + + const ipv4Pattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + const ipv6Pattern = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; + + if (!ipv4Pattern.test(ip) && !ipv6Pattern.test(ip)) { + throw new AppError( + 'IP地址格式不正确', + 400, + 'VALIDATION_ERROR' + ); + } + } + + /** + * 验证用户代理字符串 + * @param {string} userAgent 用户代理 + * @throws {AppError} 验证失败时抛出错误 + */ + validateUserAgent(userAgent) { + if (!userAgent) return; + + // 检查用户代理长度和基本格式 + if (userAgent.length > 500) { + throw new AppError( + '用户代理字符串过长', + 400, + 'VALIDATION_ERROR' + ); + } + + // 检查是否包含可疑内容 + const suspiciousPatterns = [ + /'); + console.log('❌ 恶意脚本应该验证失败'); + } catch (error) { + console.log('✅ 恶意输入验证正确失败:', error.message); + } + } + }, + { + name: '八字数据验证', + test: () => { + try { + const validData = { + name: '测试用户', + birth_date: '1990-01-15', + birth_time: '14:30', + gender: 'male' + }; + inputValidator.validateBaziData(validData); + console.log('✅ 完整八字数据验证通过'); + + const invalidData = { + name: '', + birth_date: '1990-02-30', // 无效日期 + birth_time: '25:30', // 无效时间 + gender: 'unknown' // 无效性别 + }; + inputValidator.validateBaziData(invalidData); + console.log('❌ 无效数据应该验证失败'); + } catch (error) { + console.log('✅ 无效数据验证正确失败:', error.message); + } + } + }, + { + name: '易经数据验证', + test: () => { + try { + const validData = { + question: '今年的事业运势如何?', + divination_method: 'time' + }; + inputValidator.validateYijingData(validData); + console.log('✅ 易经数据验证通过'); + + const invalidData = { + question: '', // 空问题 + divination_method: 'invalid_method' // 无效方法 + }; + inputValidator.validateYijingData(invalidData); + console.log('❌ 无效易经数据应该验证失败'); + } catch (error) { + console.log('✅ 无效易经数据验证正确失败:', error.message); + } + } + }, + { + name: '输入清理测试', + test: () => { + const maliciousInput = '用户名'; + const cleaned = inputValidator.sanitizeInput(maliciousInput); + console.log('原始输入:', maliciousInput); + console.log('清理后:', cleaned); + console.log('✅ 输入清理功能正常'); + } + }, + { + name: '时区验证测试', + test: () => { + try { + inputValidator.validateTimezone('Asia/Shanghai'); + console.log('✅ 标准时区验证通过'); + + inputValidator.validateTimezone('+08:00'); + console.log('✅ UTC偏移格式验证通过'); + + inputValidator.validateTimezone('Invalid/Timezone'); + console.log('❌ 无效时区应该验证失败'); + } catch (error) { + console.log('✅ 无效时区验证正确失败:', error.message); + } + } + } +]; + +// 运行所有测试 +console.log('开始运行测试用例...'); +console.log(''); + +testCases.forEach((testCase, index) => { + console.log(`${index + 1}. ${testCase.name}`); + try { + testCase.test(); + } catch (error) { + console.log('❌ 测试执行失败:', error.message); + } + console.log(''); +}); + +console.log('=== 测试完成 ==='); +console.log(''); +console.log('📊 测试总结:'); +console.log('- 输入验证功能已增强'); +console.log('- 边界情况处理完善'); +console.log('- 安全性验证加强'); +console.log('- 错误处理机制优化'); \ No newline at end of file