输入与输出:注入、XSS、SSRF 与文件上传的四条边界

输入与输出只有四条边界:参数化查询、按上下文转义、出站域名白名单、上传类型白名单加隔离存储。本文用 0 处字符串拼接、3 处渲染上下文、8 个域名与每月 294 MB 上传量,复算每条边界各自守住什么、漏掉之后会付出什么。

漏洞很少来自没想到的输入,多半来自想到了却没在出口处拦住。

一、一个叫 ../.env 的看板名

十一月的周二下午,司南给任务板加导出功能。导出任务拿到看板名,拼成文件名,写进临时目录,再把路径塞进 Content-Disposition。测试时他用了「产品路线图」,一切正常。

上线第三天,日志里出现一条 404,路径是 /exports/../.env。有人把看板改名成 ../.env,导出任务照做,试图把文件写到上一级目录。同一天还有第二个名字,里面带了一个换行符,让响应头多出一行。两个名字都被后续的目录校验挡住了,但那一晚他意识到:导出功能本身没问题,问题是他把用户给的字符串当成了路径和协议头的一部分。

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

输入的问题不在输入本身,在它去了哪里。 同一个看板名,进数据库、进 HTML、进文件名、进请求头,需要四种不同的处理;只在入口做一次「过滤特殊字符」,四件事一件都没解决。

四条边界是四个出口。 数据库查询、HTML 渲染、服务端出站请求、文件写入与下载,各自有自己的默认动作;边界的意思不是「检查一下」,而是「不按这个动作就走不通」。

默认动作比检查清单可靠。 参数化查询是默认,转义函数按上下文选是默认,出站只允许白名单是默认;清单会被跳过,默认不会。

一次演练比十次代码审查更能说明问题。 八个内网变体打过去,全拦住,才知道白名单是真的;全放过,也知道问题在哪一行。

二、四条边界与各自的默认动作

四条边界覆盖的是「数据离开你的代码」的四个方向,不是全部输入校验。每条只记一件事:默认动作是什么。

边界默认动作缺失时的后果当前状态
数据库参数化查询,占位符绑定注入可读全库0 处字符串拼接 SQL
HTML按上下文转义,模板自动转义存储型 XSS3 处渲染上下文各有规则
出站请求域名白名单,先解析后比对SSRF 打到内网白名单 8 个域名
文件上传类型白名单 + 隔离存储 + 签名 URL恶意文件被当页面执行6 种 MIME、10 MB、15 分钟

参数化是唯一被允许的写法。 41 个直接依赖里有两个查询构造器,都能拼字符串;项目约定是只用占位符,代码审查只查这一条。全库 0 处拼接,不是因为大家自觉,是因为拼出来的语句在测试里跑不过 lint 规则。

转义要按上下文选,不是统一转一遍。 HTML 正文、HTML 属性、内联 JS 三处的规则不同:正文转 &<>,属性还要转引号,内联 JS 要按 JS 字符串或 JSON 序列化处理。用错一处的典型症状是页面显示 &amp;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 演练全部拦截,且日志能逐条说明拒绝原因;说不出原因,等于没拦住。

使用边界与实操验收

四条边界处理的是数据离开代码的四个方向,不覆盖业务逻辑漏洞。

边界不等于输入校验。 金额为负、状态跳转越权、重复提交,这些是业务规则问题;参数化与转义都挡不住,要靠权限校验与状态机。

转义依赖模板的默认行为。 换模板引擎、换前端框架,默认可能变;每次升级后拿三处上下文各跑一次注入用例,确认默认仍然生效。

白名单会随业务漂移。 新增一个第三方服务就要改一次配置;半年不看的白名单,通常已经多了两三个「临时加的」域名。

演练要真的发请求。 只在代码里读一遍判断逻辑不算演练;八个变体打过去,看的是实际拦截结果与日志,不是读起来对不对。

本文不构成安全保证。 四条边界只降低已知类型的风险,具体实现与合规要求以你的技术栈、司法辖区与专业意见为准。

关键参考与核验范围

链接只用于核对一般做法,具体实现以各自技术栈为准。

以上链接于 2026-10-26 实测可公开访问;未核验版本、配额与浏览器兼容范围。核验日期:2026-10-26。

范围上只讨论这四条边界的默认动作与演练方式,不讨论 WAF 规则、运行时防护与业务逻辑漏洞;那些属于别的题目。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元