|
上个月帮一个朋友看他的应用小电商站,登录接口返回的错误报错信息是“用户名不存在”。我试了三个手机号,信息两个能确认已注册,漏洞一个没注册,何修页面给出的应用反馈完全不同。这意味着任何人写个脚本,错误就能批量探测出哪些手机号在平台注册过。信息攻击者拿到这份名单,漏洞要么撞库,何修要么直接发诈骗短信。应用一个报错文案,错误把用户数据拱手送人。信息 这类问题在开发里太常见了。漏洞程序员写代码时习惯把异常直接抛给前端,何修方便自己调试。catch (e) { res.json({ error: e.message }) } 一行搞定,省事。可e.message里可能带着数据库表名、SQL片段、文件路径、甚至连接字符串。上线之后这些信息就成了攻击者的地图。 修复的核心思路只有一个:对外输出模糊,对内记录详细。用户看到的应该是“操作失败,请稍后重试”或者“用户名或密码错误”,而完整的堆栈信息写进服务端日志,只有运维能看。 具体怎么做?拿登录场景举例。原始代码可能是先查数据库,查不到就返回“用户不存在”,密码不对就返回“密码错误”。改成统一返回“用户名或密码错误”,攻击者就无法区分是账号不存在还是密码错了。注册场景同理,不要提示“该邮箱已被注册”,改成“如果该邮箱可用,你将收到一封确认邮件”。多一步跳转,少一堆泄露。 有人会担心用户体验变差。用户输错密码,你告诉他“用户名或密码错误”,他确实不知道到底哪个错了。但实际数据是,绝大多数用户两种信息都记得住,偶尔输错一次重试就行。安全性和便利性在这里的权衡,天平应该偏向安全。更何况现在有验证码、有频率限制,暴力破解的成本已经很高,不需要靠精确报错来“帮助”用户。 再往深一层,全局异常处理要接管所有未捕获的错误。Node.js里用process.on('uncaughtException')兜底,Java用@ControllerAdvice,Python用中间件。原则是:任何异常到达用户之前,都被替换成通用文案。原始错误对象序列化后写入日志文件或日志服务,附带请求ID、时间戳、用户标识。排查问题时用请求ID去日志里捞,而不是让用户截图报错页面。 还有一个容易忽略的点:HTTP状态码也会泄露信息。有些接口在资源不存在时返回404,权限不足时返回403,参数错误返回400。攻击者通过状态码就能枚举出哪些资源存在、哪些接口需要什么权限。修复方式是把这些差异抹平,统一返回200加通用错误码,或者统一返回400。具体选哪种看你的API规范,关键是别让状态码成为侧信道。 我见过一个反例。某SaaS后台的导出功能,任务失败时返回的JSON里带着完整的Python traceback,里面有一行是psycopg2.OperationalError: FATAL: password authentication failed for user "admin"。数据库用户名直接暴露。更离谱的是,这个报错在前端弹窗里原样展示给用户看。后来他们被拖库,复盘时发现攻击者就是从这个报错里拿到了数据库用户名,再配合弱密码爆破进去的。 修复动作本身不复杂,难的是养成习惯。每次写catch块的时候多问一句:这个信息给用户看安全吗?Code Review的时候把错误处理当成必查项。测试环境可以保留详细报错方便调试,但上线前一定要有一道检查,确保生产环境的错误输出经过脱敏。 工具层面,Sentry、Bugsnag这类错误监控服务能帮你自动收集服务端异常,前端只展示一个错误ID。用户报错时把ID念给你,你去后台一查,堆栈、请求参数、用户环境全都有。既不影响排查效率,又不泄露任何内部信息。花半小时接入,省掉以后无数个不眠之夜。 |