尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Web安全完全指南:按角色拆解威胁模型、XSS、CSRF与部署加固

Web安全完全指南:按角色拆解威胁模型、XSS、CSRF与部署加固 做 Web 这块时间长了会越来越相信一个判断真正把人坑惨的安全事故绝大多数不是什么高深的零日漏洞而是几个早被写进教科书的低级问题——一个没做输出编码的评论框、一份被写进前端打包产物的密钥、一个默认打开目录列表的服务器配置。这些问题有个共同特点修复成本极低但被发现的时间往往很晚因为业务跑起来一切正常没人会主动去看。这篇 Web 安全完全指南我想按你站在哪一侧来组织。因为普通用户、前端开发、后端开发、服务器运维面对的威胁模型完全不一样防护技巧也完全不同混在一起讲只会让人记住一堆名词却落不了地。前两章给非技术读者讲清楚上网这件事本身的风险点和可操作的开关后面四章给开发者按前端、后端、部署、排查四个维度拆开每个点都给到能直接抄的配置和代码。文章里涉及的参数计算、配置取舍、排查路径都会说明背后的原因而不是甩一份清单就完事。1. 先把威胁模型想清楚再谈防护技巧安全圈有句话不知道敌人在哪所有防护都是心理安慰。很多人一上来就照着各种漏洞榜单从头刷一遍结果把时间花在自己系统根本不存在风险的模块上真正暴露的面却一直没管。所以第一章不聊具体技术先把谁在攻击你、从哪进来、你的资产是什么这三件事理清。1.1 三类角色三套完全不同的威胁模型我习惯把参与者分成三类每类的关注点差异极大普通使用者核心资产是账号、支付凭证、聊天记录、照片文件。攻击者动机主要是牟利和批量撒网手法集中在钓鱼页面、仿冒登录、恶意扩展。你的对手通常不是针对你个人的而是自动化的脚本在扫全网。应用开发者核心资产是业务数据、用户隐私、服务可用性。攻击者动机包括数据窃取、薅羊毛、勒索、竞品干扰。对手会针对你的具体接口做定向测试因为在黑产链条里一套可复用的接口漏洞意味着持续的收益。服务与运维方核心资产是整个运行环境、密钥体系、备份数据、其他同环境业务。攻击者一旦进来目标通常是横向扩散和长期驻留。这个层面的对手最有耐心也最难发现。把这三类混在一起谈安全就会出现一种典型错位技术人员给普通用户讲跨站脚本普通用户听完不知道该干嘛非技术人员给开发者讲别点陌生链接开发者觉得你在浪费他时间。所以后面每一章我都会明确标注这一章是给谁看的。1.2 一条请求走过的每一跳都是攻击面一次网页访问从你在地址栏敲下域名到页面渲染出来中间经过的环节比大多数人想的多。我在排查线上问题时习惯把它画成一条链路每个环节标注这里可能出什么事环节正常职责常见风险点域名解析把域名翻译成地址解析记录被改动、劫持到仿冒站点传输加密保证内容不被中途读取篡改证书过期、降级到明文、混合内容边缘缓存就近返回静态资源缓存键设计不当把用户私有页面缓存成公共资源Web 服务器处理静态文件与转发请求目录列表、备份文件、默认页、错误页泄漏版本信息应用逻辑鉴权、业务处理越权、注入、逻辑绕过数据存储持久化数据弱口令、未授权访问、备份外泄浏览器渲染执行页面脚本跨站脚本、恶意扩展、第三方脚本这张表的价值不在完整性而在于它提供了一个排查框架。任何一次事故你都可以沿着链路问发生在哪一跳上游有没有可能下游是否已经被影响我见过太多团队上来就查代码查了三天最后发现是缓存层把带登录态的页面缓存了。1.3 安全投入的性价比排序别照单全收预算和精力永远有限所以顺序很重要。我的排序原则是三个维度加权修复成本、被利用概率、后果严重度。按这个逻辑最该先做的通常是这几件全站强制加密传输、依赖组件及时更新、数据库不开公网访问、后台管理入口做访问限制、日志留存到位。这几件事的共同点是配置层面就能解决一次改完长期受益。而诸如自研加密算法自建风控模型这类除非你确实有对应的团队和场景否则投入产出比极低还容易因为自研引入新问题。安全的本质是风险管理不是把所有能做的都做一遍。2. 普通使用者视角管好浏览器和账号能挡掉八成麻烦这一章不涉及任何代码写给完全不懂技术的读者。如果你的日常工作就是写代码可以跳过但建议还是扫一眼因为你自己也是某个系统的使用者很多事故恰恰发生在开发者的个人账号上。2.1 密码管理的核心不是复杂而是不重复我见过太多人把密码设计得极其复杂然后在十几个网站复用同一个。这其实是把安全性押在最弱的那一环上——只要其中任意一个网站发生数据泄漏攻击者拿到的用户名密码组合就会被拿去批量尝试其他站点这就是所谓的撞库。你的密码再复杂也架不住它出现在别人的泄漏库里。正确的做法是用密码管理器每个站点一个独立的高强度随机密码你只需要记住一个主密码。它的原理不复杂主密码经过密钥派生函数处理后生成一把密钥用它加密密码库文件每个站点的密码是随机生成的彼此之间没有任何推导关系。即使某个站点被拖库泄漏的也只是那一个随机字符串对其他账号毫无帮助。关于两步验证这里有个选择顺序值得说明。常见形式有三种短信验证码、基于时间的一次性密码通常叫验证器应用、硬件密钥。安全强度是递增的便利性大致是递减的。验证方式抗钓鱼能力主要弱点短信验证码弱号码被接管、短信被转发验证器应用中钓鱼页面可实时转发验证码硬件密钥强需要额外携带设备如果不方便用硬件密钥至少把重要账号邮箱、支付、代码托管全部开启验证器应用形式的两步验证。特别是邮箱它是绝大多数账号的找回入口邮箱一旦失守其他账号基本等于不设防。2.2 浏览器里有几个开关值得花五分钟改掉浏览器是你和互联网之间的中间层它的默认配置偏便利需要手动收紧几项。第一是自动填充和保存密码。如果你已经用了密码管理器浏览器自带的保存功能建议关掉避免密码散落在多个地方也避免在公共设备上被读取。如果没用管理器那浏览器保存至少比手动输入到仿冒页面要好但前提是你确认当前地址正确。第二是扩展权限审查。安装浏览器扩展时页面会列出它要求的权限。一个网页截图工具要求读取和更改你访问的所有网站上的数据这个权限范围明显超出它的功能需要。这类扩展一旦更新版本被恶意代码注入它就能读取你所有页面内容包括登录后的页面。我的做法是定期清理不用的扩展保留的扩展尽量限定在特定站点生效而不是全站生效。第三是第三方 Cookie 与站点数据。浏览器现在普遍允许你限制第三方 Cookie这会顺带解决一部分跨站追踪问题。副作用是某些依赖第三方嵌入的登录或评论功能会失效取舍看你自己的使用习惯。第四是混合内容提示。一个页面本身是加密传输的但里面嵌了明文的图片或脚本浏览器会提示。这类提示不要习惯性忽略因为它意味着页面上有一部分内容是可被中途篡改的。2.3 钓鱼识别的可靠依据是地址结构不是页面长相仿冒页面现在做得越来越像靠肉眼看排版已经不可靠。但有几点是攻击者很难同时伪装的。看域名的主域名部分也就是最靠右的那一级。形如login.example.com.verify-secure.top这种地址真正的主域名是verify-secure.top前面那一长串只是子域名可以随便起。很多人扫一眼看到开头有熟悉的名字就放松了这是最常见的误判。看的时候从右往左读读到第一个斜杠前的最后两段那才是真正的站点归属。再看地址栏的加密标识。加密传输只代表这条链路是加密的不代表这个站点是可信的。个人可以花几分钟给任意域名配上证书所以看到加密标识不等于安全。它只有一个意义你填写的内容不会被中途的人读到。最后是话术和紧迫感。账号异常请立即验证您的订单存在风险24 小时内处理系统升级请重新登录这类制造时间压力的表达是钓鱼的标配。因为时间压力会让人跳过核对地址这一步。遇到任何要求输入密码、验证码、银行卡信息的页面退出去从自己收藏的入口重新进一次这一步能挡掉绝大多数钓鱼。2.4 公共网络环境下的取舍原则在酒店、咖啡厅、机场这类共享网络里同一网络下的其他设备理论上能看到一部分广播流量也可能存在被伪造的接入点。这不是说不能用而是要知道什么该做什么不该做。可以放心做的浏览公开信息、看视频、查资料。因为这些站点的内容本身就没什么机密性而且主流站点都启用了加密传输。需要谨慎的登录网银、输入支付密码、处理工作邮箱里的敏感附件。如果你确实需要优先用手机自带的移动数据这比共享网络可控得多因为链路上少了未知的一跳。还有一个常被忽略的点是设备隔离。家里的路由器如果能开访客网络把智能音箱、摄像头、扫地机器人这些联网设备单独放一个网段和你的电脑手机分开。这类设备的固件更新往往不及时一旦被攻破同网段的其他设备就会暴露在同一个攻击面里。3. 前端开发视角跨站脚本、跨站请求伪造与内容安全策略从这一章开始进入开发视角。前端安全有个特殊性很多问题不是前端制造的但必须在渲染环节被关掉。换句话说后端的职责是保证数据准确前端的职责是保证数据被当作数据而不是代码对待。3.1 跨站脚本的根因不是过滤不足而是上下文混淆绝大多数跨站脚本问题的本质是把用户可控的数据放进了代码执行的位置。这里的关键词是两个用户可控、代码位置。只要同时满足这两条就存在风险无论你做了多少过滤。过滤为什么不可靠因为黑名单永远列不全。攻击者可以用编码变形、事件属性、SVG 内联脚本、模板字符串等无数种方式构造载荷而你要穷举所有变体这不现实。可靠的做法是按上下文做输出编码。因为同一个字符串放进不同的位置需要转义的字符完全不同输出位置必须处理的字符常用做法HTML 文本节点 HTML 实体编码HTML 属性值加上引号再转义 尽量用引号包裹避免无引号属性JavaScript 字符串转义引号、反斜杠、换行优先用数据传递而不是字符串拼接URL 参数做 URL 编码拼接前对每个参数单独编码CSS 属性基本不要放用户输入如必须做严格白名单校验前端框架在这方面帮了大忙。React、Vue 默认会把插值内容当作文本处理所以下面这种写法是安全的// 安全内容作为文本节点渲染 return div{userComment}/div;而下面这种写法绕过了框架的保护// 危险直接把字符串当 HTML 解析 return div dangerouslySetInnerHTML{{ __html: userComment }} /;我的建议是在代码规范里把这类跳过框架保护的写法列为需要评审的操作用搜索工具定期扫一遍代码库看看有多少处。数量往往超出预期因为很多是为了渲染富文本评论加进去的后来就没人再管了。如果业务确实需要渲染富文本正确做法是用成熟的白名单式净化库先净化再渲染而不是自己写正则替换。自己写正则的结果通常是上线一周后被发现绕过。3.2 内容安全策略怎么配才有效从观察模式开始内容安全策略是前端防线的第二层它的思路是告诉浏览器这个页面只允许加载哪些来源的脚本。和输出编码的区别在于输出编码是堵住某个具体的注入点策略则是即使有注入点也让注入的脚本执行不了。配置方式是通过响应头下发。最忌讳的做法是从网上抄一段配置直接上线因为稍有不慎就会把业务功能全部打挂。我的推荐路径是分三步走。第一步先用仅报告模式跑一到两周Content-Security-Policy-Report-Only: default-src self; script-src self; report-uri /csp-report这个模式下浏览器只报告违规行为不拦截。你要准备一个能接收报告的服务端接口把收到的违规记录按来源域名和指令分类统计。第二步根据报告结果逐个放行真实依赖。常见的情况是需要放行对象存储域名、接口域名、埋点域名。这时候不要图省事用通配符而是精确到具体域名。第三步把仅报告改成强制模式同时保留报告地址方便持续监控。有几条经验值得强调。内联脚本是最大的障碍如果页面里有大量script内联代码或onclick属性你会在报告里看到满屏违规。解决办法是给内联脚本加随机数或哈希值而不是直接放开unsafe-inline。unsafe-eval同样要慎用有些老模板引擎依赖它但放开等于给注入留下了执行环境。升级到严格模式有个技巧用随机数配合strict-dynamic可以让由信任脚本动态创建的脚本继续被信任这样不用把每个动态加载的地址都列出来。3.3 跨站请求伪造的三重防线缺一不可跨站请求伪造的原理是浏览器会自动带上目标站点的 Cookie所以攻击者只要在自己的页面里诱导你发起一个请求你的身份凭证就自动附上了。关键在于浏览器无法区分这个请求是你主动点的还是被别人诱导的。第一道防线是Cookie 属性配置。给会话 Cookie 加上SameSiteLax浏览器的默认行为就变成跨站发起的表单提交不携带这个 Cookie。对于绝大多数业务这已经能挡掉大部分场景。如果业务涉及跨站嵌入需要细分成SameSiteNone; Secure但那种场景必须配合下面的第二道防线。第二道防线是请求令牌。服务端生成一个和当前会话绑定的随机值写进页面提交时带回服务端比对。要点是令牌必须随机、必须与会话绑定、必须服务端校验。我见过有人把令牌存在 Cookie 里再和 Cookie 比对这等于没做——攻击者诱导的请求会同时带上两个相同的值。第三道防线是校验请求来源头。对于涉及状态变更的接口检查请求来源头是否属于本站允许的域名列表。这一层的价值在于它能覆盖一些前端框架自动处理、容易漏掉的接口。三道防线叠加的逻辑是任何一道单独都有绕过可能但同时绕过的难度会显著提升。3.4 依赖组件随手引入的每个包都是别人写的代码前端项目的依赖树动辄几百个包其中绝大多数你不是主动选的而是被间接引入的。这意味着你的安全性部分取决于这些包的维护者。最基本的动作是定期运行依赖审计看看有没有已知问题# 检查依赖中的已知问题 npm audit # 只看高危及以上 npm audit --audit-levelhigh然后配合锁文件固定版本避免构建时悄悄升到未验证的版本。同时关注包名相似的情况这是常见的手法把常用包名微调一个字符等着别人打错。我的习惯是引入依赖前看一眼下载量、维护者、最近更新时间特别新的包或者长期不更新的包都要多想想。再往下还有一层是构建产物泄漏。前端打包文件是公开的任何人打开开发者工具都能看到。所以配置信息绝对不能进前端代码包括各种密钥、内部服务地址、调试开关。有些项目把环境变量写进前端构建配置以为加了前缀就安全了实际上那只是打包进了产物前缀只是暴露在代码里的一个变量名而已。4. 后端与服务端视角注入、认证、会话与文件处理后端是数据的最后一道门。前端可以出错但后端不能把错误数据直接落库。这一章挑四个最容易出问题、也最容易一次性解决的方向。4.1 结构化查询语言注入参数化查询是唯一正解注入的本质是数据被拼接进了语句结构导致数据被当作语法解析。防御方式不是过滤特殊字符而是让数据和语句结构彻底分离。错误写法sql SELECT * FROM users WHERE name name cursor.execute(sql)正确写法sql SELECT * FROM users WHERE name %s cursor.execute(sql, (name,))这两段的差别在于第一种情况下如果name是 OR 11整条语句的逻辑就被改变了。第二种情况下驱动会把参数作为数据传给数据库引擎无论内容是什么都不会改变语句结构。这里有个容易被忽略的细节参数化只对值有效对表名、列名、排序方向无效。也就是说如果你要根据前端传来的字段名排序不能写成占位符因为那样语法就是错的。这类场景必须用白名单映射ALLOWED_SORT {created_at: created_at, name: name} sort_field ALLOWED_SORT.get(request.args.get(sort), created_at)类似的还有动态条件拼接。如果确实需要根据条件拼不同片段拼接的部分必须是代码里写死的常量不能来自请求参数。注意批量导入、导出、报表查询这类功能往往是注入的高发区因为开发时为了灵活性容易走字符串拼接。上线前对这些模块单独做一次审查。4.2 认证与会话管理的常见误用会话这块的坑集中在令牌设计上。以常见的 JSON Web Token 为例我被问到最多的问题是为什么用了令牌还是能被冒用。梳理下来大致是这几类。把敏感信息放进载荷。令牌的载荷是 Base64 编码不是加密任何人拿到都能解开看。所以别放手机号、身份信息这类内容。有效期设得太长。有些项目为了省事把有效期设成一个月甚至更长。正确做法是访问令牌短一些十几分钟到一小时配上刷新令牌来续期。这样即使令牌泄漏窗口期也可控。缺少撤销机制。令牌一旦签发就无法撤销这在用户改密码、下线设备、发现异常时非常被动。解决办法是在服务端维护一个失效名单或者在令牌里带上版本号用户信息变更时递增版本号校验时比对。前者的实现成本更低。签名算法校验不严。校验时一定要显式指定允许的算法不能让令牌自身声明用什么算法就用什么算法否则可能被构造出绕过签名的令牌。放在本地存储里。这个位置能被同源的脚本读取一旦页面存在注入问题令牌就直接到手。相比之下配合会话属性配置的 Cookie 更可控因为它可以设置成脚本不可读。登录接口本身也有几个要点无论如何都返回统一的错误提示不要区分用户不存在和密码错误否则等于给攻击者提供了枚举有效账号的接口对登录失败做频率限制和递增延迟密码存储必须用专门的派生算法如 bcrypt、scrypt、Argon2绝对不要用普通哈希函数加盐的土办法。4.3 文件上传与路径处理两个高危动作文件上传之所以危险是因为它把客户端的数据变成了服务端的文件。如果处理不当上传一个脚本文件再访问它就等于在服务器上执行了任意代码。防护要点按重要性排列存储位置必须在网站根目录之外让上传的文件无法通过地址直接访问只能通过应用层做鉴权后读取。校验文件类型要基于内容而不是扩展名扩展名可以随便改。读文件头判断真实类型再和允许列表比对。重命名文件用随机字符串做文件名不要沿用用户提供的名字。这一步同时解决了路径穿越和覆盖已有文件的问题。限制大小和数量避免磁盘被塞满。如果允许图片上传后重新编码一次这能顺带清掉图片里夹带的额外内容。路径穿越则是另一个经典问题用户传入的路径里带../如果直接拼接进文件读取逻辑就能读到预期之外的文件。防御方式很直接——取文件名部分丢弃任何目录信息然后拼接到固定的基目录下String base /data/uploads; String name Paths.get(userInput).getFileName().toString(); Path target Paths.get(base).resolve(name).normalize(); if (!target.startsWith(Paths.get(base))) { throw new IllegalArgumentException(非法路径); }最后那句归一化后的前缀检查是必须的因为字符串替换的方式容易被各种编码变形绕过。4.4 日志与脱敏出事时能不能查得清这一节常被排到最后但它的价值在事故发生后会成倍体现。核心是两件事记什么、不记什么。该记的登录成功与失败区分账号和来源、权限变更、敏感数据的读取和导出、金额相关的操作、接口调用的关键参数摘要。这些记录决定了你能不能还原一次异常操作的完整路径。不该记的明文密码、完整的令牌、完整的证件号、银行卡号、完整的身份信息。如果排查确实需要做掩码处理只保留末尾几位。还有一点是日志本身也是攻击面。日志如果被注入换行符可能造成记录伪造让攻击痕迹被覆盖。所以写入前要对换行和分隔符做转义。日志文件的访问权限也要收别让它能被匿名读取。5. 服务与部署层面几行配置就能降掉一半风险这一章的目标是花最少的时间拿到最大的收益。以下每一项都是配置层面的改动不涉及业务代码重构。5.1 安全响应头清单逐条说明用途响应头是最省力的加固手段加上去不会有性能损耗也不会影响业务逻辑。常用的几条及其作用响应头作用推荐值思路Strict-Transport-Security让浏览器后续强制走加密传输先设较短时间确认无问题后延长Content-Security-Policy限制脚本、样式、资源的加载来源从仅报告模式起步逐步收紧X-Content-Type-Options禁止浏览器猜测内容类型固定为nosniffReferrer-Policy控制跳转时携带的来源信息常用strict-origin-when-cross-originX-Frame-Options防止页面被嵌套在别的站点里用frame-ancestors更灵活配置示例以通用服务器配置为例add_header Strict-Transport-Security max-age15552000; includeSubDomains always; add_header X-Content-Type-Options nosniff always; add_header Referrer-Policy strict-origin-when-cross-origin always;注意always参数它的作用是让这些头在错误响应里也生效。不加的话返回错误页时可能就丢了。关于Strict-Transport-Security有个实操建议先设一个较短的时间值上线观察几天。因为一旦浏览器记住了这条策略在有效期内即使证书出问题用户也无法通过忽略警告继续访问属于不可逆操作。确认全站加密传输没有遗漏后再逐步延长。5.2 传输加密的配置要点加密传输的配置重点不在开没开而在开得对不对。几个常见问题协议版本过旧。只保留较新的版本老版本存在已知的弱点浏览器端也早已不再信任。加密套件过于宽松。优先选择支持前向保密的套件这样即使长期密钥泄漏历史流量也无法被解密。证书链不完整。有些环境缺少中间证书浏览器在不同平台上表现不一致部分用户会看到警告。配置时把完整的证书链一起下发。证书到期没有提醒。这是最不应该发生但实际高频的事故。现在自动签发和续期已经很成熟建议直接开自动续期同时保留一个独立的到期监控作为兜底。混入了明文资源。页面本身加密但引用了明文的图片或脚本浏览器会警告而且这部分内容可被篡改。上线前用开发者工具的网络面板扫一遍把所有明文引用改成加密地址。5.3 权限最小化与文件泄漏服务器上最常见的问题不是被攻破而是本来就不该被访问的东西一直敞着。目录列表。服务器默认开启时访问一个没有默认页的目录会列出所有文件。这个功能对攻击者来说等于一份免费的资源清单。关掉它一行配置的事。备份文件和临时文件。编辑器产生的临时文件、打包前的源码压缩包、数据库导出文件这些如果放在网站根目录下就可能被直接下载。防御方式是双重的构建流程里禁止把这类文件发布出去同时服务器层面拦截常见后缀的请求。location ~* \.(bak|swp|sql|tar|gz|zip|log)$ { deny all; }默认页和版本信息。默认安装页、状态页、接口文档页、服务器版本信息这些都应该在生产环境关闭或者加上访问限制。我见过把内部接口文档直接挂在公网上的情况等于把攻击面画了张地图递过去。运行账号权限。应用进程不要用最高权限账号运行。单独建一个账号只给它需要的目录读写权限这样即使应用被攻破能造成的破坏也有限。5.4 限流与暴力尝试防护这部分的思路是让攻击成本变高高到不值得。登录接口需要按账号和来源两个维度分别限流。只按来源限流的话攻击者用大量不同来源轮换就绕过了只按账号限流的话会误伤正常用户比如用户自己输错了五次。两个维度叠加取较严格的策略。推荐做法是递增延迟而不是直接封禁。第一次失败等一秒第二次两秒依此类推。这种方式对正常用户几乎无感但对需要尝试上万次的脚本来说时间成本会指数级上升。相比之下直接封禁账号容易被恶意利用——攻击者故意输错别人的账号来触发封禁这就变成了拒绝服务。其他接口按业务特点设置阈值。查询类接口可以宽松些涉及短信发送、验证码、导出、支付这类有成本的接口要严格控制同时加上验证码或者二次确认。还有一层是监控异常模式。单个来源在短时间访问大量不同账号、同一账号从相距很远的位置登录、非工作时间的批量导出这些都是值得告警的信号。监控不需要很复杂关键是有人看并且有明确的处置流程。6. 常见问题排查速查表与踩坑记录前面讲的是怎么防这一章讲出了问题怎么办。我在实际处理中总结了一些规律放在这里供参考。6.1 从症状到原因的排查路径排查的思路是先定位环节再定位原因。不要一上来就翻代码那样效率很低。症状优先排查方向常见原因页面偶尔弹出脚本执行异常前端注入点、第三方脚本富文本渲染未净化、第三方脚本被改动用户登录状态串号缓存层、会话标识生成缓存键未区分用户、会话标识随机性不足出现异常的批量数据读取接口鉴权、日志接口只校验登录未校验权限、缺少访问频率限制服务器对外发起异常连接依赖组件、上传目录依赖包被投毒、上传目录可执行证书警告证书链、到期时间中间证书缺失、自动续期失败排查时有个技巧先看时间线再看代码。把异常行为的发生时间、来源、涉及账号列出来往往能直接指向某个功能模块。我之前遇到过一次数据异常读取翻了半天代码没头绪后来按时间排序日志发现全部集中在同一个导出接口而那个接口是三个月前临时加的一直没做权限校验。6.2 我实际踩过的几个坑第一个坑在仅报告模式下配了策略就忘了切强制。跑了两周报告看违规数量降下来了以为已经生效实际上一直是观察状态没有拦截能力。后来在代码评审里加了检查项把响应头配置纳入上线清单。第二个坑把令牌存在了本地存储里。当时觉得方便前后端分离的项目里这样跨域也好处理。直到做安全评审时被指出页面只要有任何一处注入令牌就直接暴露。改成会话属性配置后顺便把跨域方案也重新设计了一遍虽然多花了点时间但这一步是必须的。第三个坑依赖锁文件没提交。本地和测试环境都没问题生产构建时依赖升到了新版本引入了一个不兼容的改动同时带进来一个已知问题。教训是锁文件必须进版本控制构建必须用锁文件安装。第四个坑日志里记了完整的请求体。出发点是排查方便结果日志文件里出现了用户的完整凭证信息。后来改成只记字段名和长度或者对值做掩码。这件事让我意识到调试便利性和数据安全需要显式取舍不能默认选方便。第五个坑上传目录在网站根目录下。当时为了能直接通过地址访问用户头像把目录放在了可访问路径里。虽然做了扩展名校验但后来发现可以通过特殊构造绕过。改成应用层读取后问题从根上消失了。6.3 上线前的自查清单我现在的习惯是每次上线前过一遍下面这张清单五分钟就能走完但能挡掉大部分低级问题。所有对外接口是否都有鉴权并且区分了登录和有权限两个层次涉及数据变更的接口是否校验了请求来源用户输入进入页面的位置是否都按上下文做了处理数据库查询是否全部使用参数化动态表名列名是否走白名单敏感配置是否只存在于服务端构建产物里有无泄漏会话 Cookie 的属性是否配置完整上传目录是否在可访问路径之外文件类型是否按内容校验传输加密是否全站覆盖有无明文资源安全响应头是否都已下发包括错误响应日志是否记录了关键操作是否做了脱敏登录和其他敏感接口是否有频率限制依赖是否有已知问题锁文件是否已提交这张清单覆盖不了所有情况但它覆盖的是高频且容易漏的部分。真要说经验最重要的一条是安全不是一次性的加固动作而是每次改动时的一个习惯。我见过太多项目在安全评审时突击整改一遍过了三个月新加的功能又回到了老样子。把上面这些检查项嵌进代码评审的模板里让它在每次合并请求时被顺带看一眼比任何一次性的专项整改都有效。还有个小技巧分享一下给项目建立一个安全决策记录文件把做过的取舍和原因写进去。比如为什么某个接口用了令牌而不是 Cookie、为什么某个策略设成了这个值、为什么某处没有做严格校验通常是业务权衡。这类信息在人员变动后极容易丢失后来的人看到一段宽松的配置不敢改也不知道该不该改最后就一直是那个状态。把原因写下来比写一堆规则说明有用得多。
返回列表