漏洞很少来自没想到的输入,多半来自想到了却没在出口处拦住。
一、一个叫 ../.env 的看板名
十一月的周二下午,司南给任务板加导出功能。导出任务拿到看板名,拼成文件名,写进临时目录,再把路径塞进 Content-Disposition。测试时他用了「产品路线图」,一切正常。
上线第三天,日志里出现一条 404,路径是 /exports/../.env。有人把看板改名成 ../.env,导出任务照做,试图把文件写到上一级目录。同一天还有第二个名字,里面带了一个换行符,让响应头多出一行。两个名字都被后续的目录校验挡住了,但那一晚他意识到:导出功能本身没问题,问题是他把用户给的字符串当成了路径和协议头的一部分。
以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。
输入的问题不在输入本身,在它去了哪里。 同一个看板名,进数据库、进 HTML、进文件名、进请求头,需要四种不同的处理;只在入口做一次「过滤特殊字符」,四件事一件都没解决。
四条边界是四个出口。 数据库查询、HTML 渲染、服务端出站请求、文件写入与下载,各自有自己的默认动作;边界的意思不是「检查一下」,而是「不按这个动作就走不通」。
默认动作比检查清单可靠。 参数化查询是默认,转义函数按上下文选是默认,出站只允许白名单是默认;清单会被跳过,默认不会。
一次演练比十次代码审查更能说明问题。 八个内网变体打过去,全拦住,才知道白名单是真的;全放过,也知道问题在哪一行。
二、四条边界与各自的默认动作
四条边界覆盖的是「数据离开你的代码」的四个方向,不是全部输入校验。每条只记一件事:默认动作是什么。
| 边界 | 默认动作 | 缺失时的后果 | 当前状态 |
|---|---|---|---|
| 数据库 | 参数化查询,占位符绑定 | 注入可读全库 | 0 处字符串拼接 SQL |
| HTML | 按上下文转义,模板自动转义 | 存储型 XSS | 3 处渲染上下文各有规则 |
| 出站请求 | 域名白名单,先解析后比对 | SSRF 打到内网 | 白名单 8 个域名 |
| 文件上传 | 类型白名单 + 隔离存储 + 签名 URL | 恶意文件被当页面执行 | 6 种 MIME、10 MB、15 分钟 |
参数化是唯一被允许的写法。 41 个直接依赖里有两个查询构造器,都能拼字符串;项目约定是只用占位符,代码审查只查这一条。全库 0 处拼接,不是因为大家自觉,是因为拼出来的语句在测试里跑不过 lint 规则。
转义要按上下文选,不是统一转一遍。 HTML 正文、HTML 属性、内联 JS 三处的规则不同:正文转 &<>,属性还要转引号,内联 JS 要按 JS 字符串或 JSON 序列化处理。用错一处的典型症状是页面显示 &lt;,或者属性被引号提前闭合。
出站请求先解析域名再比对。 只比对字符串会被 evil.com#allowed.com、十进制 IP、重定向绕过;正确顺序是解析出 IP,确认不是内网段,再发请求,且不跟随跳转。
上传的三道闸缺一不可。 大小 10 MB 挡的是压缩炸弹与磁盘占满,6 种 MIME 挡的是可执行文件,隔离存储加 15 分钟签名 URL 挡的是「上传目录被当静态资源访问」。
边界要写在代码结构里,不写在注释里。 所有出站请求走同一个封装函数,所有上传走同一个入口;绕过封装需要显式改代码,而不是忘了调用某个检查。
入站校验与出站约束是两件事。 入站校验回答「这个值合不合法」,出站约束回答「这个值能不能到那里」;只做前者,合法但不该发的值照样发出去。
每类边界只允许一个入口。 数据库走 ORM 的查询方法,出站走封装,上传走上传函数;多一个入口就多一处可能忘记。
三、三处渲染上下文与八次 SSRF 演练
渲染上下文的规则表只有三行,但每行对应一类真实事故。
| 上下文 | 转义规则 | 典型错误 | 检查方式 |
|---|---|---|---|
| HTML 正文 | 转 & < > " ' | 富文本直接输出 | 模板默认转义,关闭需两人同意 |
| HTML 属性 | 额外转引号,属性值一律加引号 | 未加引号导致属性注入 | 静态扫描未加引号的属性 |
| 内联 JS | 序列化为 JSON,禁止字符串拼接 | 用户值拼进脚本 | 代码审查只看这一条 |
富文本是唯一允许保留 HTML 的地方。 它走单独的清洗库,白名单标签与属性,输出前再跑一次;其余所有位置一律按上表处理。
CSP 是第二道,不是第一道。 响应头里禁止内联脚本与外部未知域,能在转义漏掉时把执行挡住;MDN 的文档写得很清楚,先有转义,再有策略。
SSRF 演练用八个变体,全部拦截。 分别是:十进制 IP、八进制 IP、IPv6 回环、localhost 别名、带用户信息的 URL、302 跳转到内网、DNS 重绑定、云元数据地址。八个都走同一个封装,日志里能看到每一次被拒的原因。
白名单是 8 个域名,不是 8 条规则。 支付回调、邮件服务、对象存储、两个 AI 提供商、日志收集、两个 webhook 目标;新增域名要改配置并发一次版,不允许运行时拼。
重定向要单独处理。 跟随跳转等于把白名单交给对方;封装里关闭自动跟随,需要跳转时逐跳校验目标域名。
错误信息不要回显用户输入。 报错页把原始值原样印出来,等于在错误处理路径上开了一个反射型 XSS;统一错误页只给编号。
四、账:每月 210 个文件与 294 MB
上传的量不大,但它是唯一会把外部字节存进自己系统的边界,所以要算清楚。
复算:
上传量 = 210 个/月 × 1.4 MB 平均 = 294 MB/月
年化 = 294 × 12 = 3,528 MB ≈ 3.4 GB/年
单文件上限 10 MB,平均 1.4 MB → 平均占上限 14%
类型白名单 6 种,超限与类型不符都直接拒,不进隔离区
出站白名单 8 个域名;SSRF 演练 8 个变体 → 8/8 拦截
渲染上下文 3 处 × 每条 1 条转义规则 = 3 条规则
边界 4 条 × 每季度复核 1 次 = 4 次/年
294 MB 不是容量问题,是审计问题。 一年 3.4 GB 存得下,但每个文件都要能回答「谁传的、什么时候、什么类型、有没有被扫描过」;四个字段缺一个,出事时就查不下去。
10 MB 是产品约束,不是技术上限。 任务板附件平均 1.4 MB,10 MB 覆盖绝大多数截图与文档;把上限提到 100 MB,收益很小,压缩炸弹与超时重试的风险却成倍上升。
6 种 MIME 要按内容判断,不只看扩展名。 读取文件头的前几个字节,与声明的类型比对;不一致就拒,不管扩展名写的是什么。
15 分钟的签名 URL 是权衡。 太短,大文件下载到一半失效;太长,链接被转发后长期可用。15 分钟覆盖 10 MB 在常见带宽下的下载时间,留了一点余量。
隔离存储要独立域名。 上传文件放在只返回下载头的域名下,不与主站共享 Cookie 与同源策略;即使某个文件被当成 HTML 打开,也拿不到主站的会话。
文件名的处理是独立的一小步。 原始名只存数据库用于展示,实际存储用随机 ID;下载时再从数据库取名,避免路径与响应头被用户字符串污染。
每月 210 个文件里约有 6 个需要人工看一眼。 类型正常但内容可疑的放进隔离区,人工确认后放行;这个比例不高,但漏一个的代价远大于看一眼。
签名 URL 要记一笔。 每次签发记下用户、文件 ID、过期时间;下载出现异常时,能对上是谁、什么时候拿的链接。
五、行动清单与复算验收
先做默认动作,再谈覆盖。
- 全库搜索字符串拼接 SQL,确认 0 处,并把 lint 规则加进 CI。
- 列出 3 处渲染上下文,每处写明转义函数与检查方式。
- 所有出站请求收敛到一个封装,关闭自动跟随跳转。
- 白名单写死 8 个域名,新增必须改配置并发版。
- 跑一遍 8 个 SSRF 变体,记录每一次拒绝原因。
- 上传入口加 10 MB 上限、6 种 MIME 白名单与文件头比对。
- 上传文件放独立域名,下载只走 15 分钟签名 URL。
- 每季度复核四条边界,确认封装没有被绕过。
验收标准:八次 SSRF 演练全部拦截,且日志能逐条说明拒绝原因;说不出原因,等于没拦住。
使用边界与实操验收
四条边界处理的是数据离开代码的四个方向,不覆盖业务逻辑漏洞。
边界不等于输入校验。 金额为负、状态跳转越权、重复提交,这些是业务规则问题;参数化与转义都挡不住,要靠权限校验与状态机。
转义依赖模板的默认行为。 换模板引擎、换前端框架,默认可能变;每次升级后拿三处上下文各跑一次注入用例,确认默认仍然生效。
白名单会随业务漂移。 新增一个第三方服务就要改一次配置;半年不看的白名单,通常已经多了两三个「临时加的」域名。
演练要真的发请求。 只在代码里读一遍判断逻辑不算演练;八个变体打过去,看的是实际拦截结果与日志,不是读起来对不对。
本文不构成安全保证。 四条边界只降低已知类型的风险,具体实现与合规要求以你的技术栈、司法辖区与专业意见为准。
关键参考与核验范围
链接只用于核对一般做法,具体实现以各自技术栈为准。
- OWASP Top Ten :用于说明注入、XSS 与 SSRF 在常见风险中的位置。
- File Upload Cheat Sheet :用于说明上传的类型白名单、大小限制与隔离存储。
- SSRF Prevention Cheat Sheet :用于说明出站请求先解析再比对与白名单做法。
- MDN CSP 指南 :用于说明内容安全策略作为转义之外的第二道防线。
以上链接于 2026-10-26 实测可公开访问;未核验版本、配额与浏览器兼容范围。核验日期:2026-10-26。
范围上只讨论这四条边界的默认动作与演练方式,不讨论 WAF 规则、运行时防护与业务逻辑漏洞;那些属于别的题目。
系列导航
- 上一篇:密钥与配置:十七个密钥、三档与泄露后的三十分钟
- 下一篇:依赖与供应链:四十一个直接依赖、一百八十六条告警
- 回到目录:一个人的安全基线:十四个入口、十七个密钥与一次 72 小时响应
- 相关阅读:AI 功能的滥用防护:配额、提示注入与输出边界
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。