第 21 篇:密钥进了版本库——轮换,不是删 commit
全清单收尾。一个密钥被 commit 进了 git,你发现后删掉文件再提交——用真实 git 仓库证明:密钥还在历史里,任何 clone 都带着它。这一篇讲清’删文件 ≠ 删历史’,以及为什么唯一可靠的修复是’假设它已泄露、立即轮换’,而不是想办法把它从历史里擦干净。
全清单收尾。一个密钥被 commit 进了 git,你发现后删掉文件再提交——用真实 git 仓库证明:密钥还在历史里,任何 clone 都带着它。这一篇讲清’删文件 ≠ 删历史’,以及为什么唯一可靠的修复是’假设它已泄露、立即轮换’,而不是想办法把它从历史里擦干净。
只要你的服务端会去访问一个用户给的 URL——抓网页标题、下载头像、调 webhook——攻击者就能把这个地址改成你的内网。这一篇用真实的服务器演示凭据是怎么被取走的,再用操作系统的 DNS 解析证明:拿主机名做黑名单在定义上就必输,因为 127.1、2130706433、0x7f000001 都解析到同一个地址。正确的修复是先解析、判断解析出来的 IP、再连到那个 IP。
只要你的代码里有 os.path.join(某个目录, 用户给的名字),就要看这一篇。用标准库演示三件事:../ 怎么跳出去;为什么给一个绝对路径时 join 会把你的基准目录整个丢掉(这一条最容易漏);以及为什么 replace(’../’,’’) 这种清洗被 ….// 一击即破。正确的修复和 SSRF 那篇是同一句话——别判断名字长什么样,判断它最终落在哪。
你收了一份 XML,只想读里面两个字段。可 XML 这门格式允许文档自己声明「这里插入某个文件的内容」,而解析器会照做——于是一次「解析」变成了一次任意文件读取,甚至一次 SSRF。这一篇用同一份文档、同一个解析器、只差一个开关,把它跑出来:关着的时候什么都没有,打开的时候密钥被读了出来。核心是一句:保护你的是一个默认值,不是一条定律。
「把请求体里的字段更新到用户对象上」是每个框架都提供的便利。问题是:请求体里有哪些字段,是攻击者说了算的。这一篇的 demo 值得单看——它的 SQL 完全参数化、一处拼接都没有,第 2 篇的功课全做对了,普通用户照样把自己变成了管理员。因为这一次,可控的不是值,是字段名。
一张只能用一次的优惠券,同时发五个请求过去,五个都兑换成功了。代码没写错任何一行:先查、再判、再改,逻辑无懈可击——只是这三步之间有缝,而并发会钻进那条缝。这一篇用真实的数据库和真实的线程把它跑出来(每次都是 5 比 1),并给出唯一可靠的修法:别把检查和动作分成两步。