09.08 输入与输出防护:IM 正文、附件和链接的不同边界
返回第九卷 · 接口与限额:09.02 · 数据访问:09.05 · 浏览器:09.06 · 授权:09.07
09.08 输入与输出防护:IM 正文、附件和链接的不同边界
DeepTutor 初稿经技术与教学审阅后的静态课程。正文、文件名、URL、错误和请求均为虚构示例;本次没有运行 Go、数据库、附件/预览服务、IM 服务或站点。本章只设计教学 IM 的防护边界,不声称检查了 OpenIM 的安全实现。
一、同一条消息离开请求后,会进入不同解释器
u-a 向 c-a 发送虚构消息 m-a。正文可能含普通文字、HTML 样式的字符或一个链接;以后产品还可能允许上传图片附件。它会经过 HTTP/JSON 解析、业务校验、SQL/文档存储、HTML/JSON 输出、日志,甚至被链接预览服务当作网络地址。一份数据在不同位置会被不同解释器理解:SQL 看查询结构,浏览器看 HTML/脚本,文件系统看路径,HTTP 客户端看 URL。不能靠入口处一个“通用清洗函数”同时解决所有位置的问题。
不受信输入:正文 / 会话ID / 附件名 / URL
→ 请求体与字段上限、类型和业务规则
→ SQL 值参数或文档字段(授权仍独立)
→ HTML文本 / JSON / 附件下载 / 链接预览 / 结构化日志
每个输出位置再按自身语义处理本章前置已齐:06.02 会写参数 SQL,09.06 会区分表单/模板/CORS,09.07 会做对象级授权。09.02 已提出三种限额:整个请求体、单条消息正文、附件或单文件。本章把它们与具体失败变式连接起来;当前课程没有运行这些接口或附件服务。
| 第一遍 | 第二遍 | 练习交付物 |
|---|---|---|
| 来源、单位、参数、输出位置 | 文件路径/类型、SSRF、日志泄露与防护组合 | 一张输入→使用位置表、三种限额、一组拒绝与确认边界 |
二、先限制能进入系统多少,再判断内容是否合法
入口需要先识别请求方法、Content-Type 和可接受格式;JSON 请求要按约定结构完整解码,不能只读到第一个对象就忽略尾部多余内容。Content-Length 可作早期判断,却不能是唯一上限:请求可能没有可信长度声明或在传输中出现不同实际字节数。Go net/http.MaxBytesReader 是未来个人实现可用的请求体限制入口;把它放在真正解析请求体之前,超限时返回受控错误,避免无界读取耗尽内存。这里只讲职责,不运行 handler。
| 上限 | 本题解释 | 超过时应在哪里停下 |
|---|---|---|
| HTTP 请求体 | JSON/表单/上传元数据及编码开销的总字节数 | 解析前或解析中有界拒绝 |
| 单条正文 | 解码后正文的 UTF-8 字节数,沿用 09.02 当前 6 字节上限 | 写消息前由服务端业务规则拒绝 |
| 单附件/总附件 | 每个文件与一次请求内附件总量 | 流式读取、存储及解码/处理前后各设边界 |
len("你好")=6,len("你好呀")=9(按 Go UTF-8 字节表示),后者超过当前 6 字节上限。三个常见汉字可占 9 字节;浏览器输入框的“字符数”不能替代后端字节预算。若业务要求非空、禁止某些控制字符、保留换行或限制消息类型,应另写合同;不要把“去掉所有标点与尖括号”当安全校验,这会损伤正常聊天文本,也不能阻止后续使用位置的注入。
附件还可能在压缩、图片解码或转码后变大;只检查上传时字节数不等于限制处理后的 CPU、内存和磁盘开销。客户端前端校验可以尽早提示,但恶意或原生客户端可绕过页面,所以每条入口都以服务端校验为准。大小验证不会替代 SQL 参数、HTML 输出转义或会话权限。
三、SQL 注入:值参数化,结构由服务端选择
06.02 的历史查询按 conversation_id 和 seq 取页。若把客户端提供的会话 ID 直接拼入 SQL,输入中带引号等字符可能改变查询结构。Go 的 database/sql 让查询结构保持固定,再把值单独传给驱动:
const q = `SELECT message_id, seq, body
FROM messages
WHERE conversation_id = $1 AND seq < $2
ORDER BY seq DESC LIMIT $3`
rows, err := db.QueryContext(ctx, q, conversationID, beforeSeq, limit)这是静态片段;数据库句柄、受信身份、Limit 校验与 Rows.Close/Err 仍按 06.10 完成。即使 conversationID 看起来像 c-a' OR '1'='1,它也只能作为一个值去匹配,不能被当作额外 SQL 条件。fmt.Sprintf 拼值或自己手写“替换引号”不应代替参数化。
参数通常绑定值,并不替你安全选择表名、列名或 ORDER BY ASC/DESC。如果界面允许“最新/最旧”两种排序,就在服务端把这两个有限选项映射到两条预定义 SQL,或白名单决定一段固定结构;不能把客户端任意字符串拼进排序语句。参数化也只挡 SQL 结构被输入改变,不挡对象级越权:已登录的 u-c 即使发送完全合法的 c-a 参数,也仍无权读 c-a。
MongoDB 也要固定查询结构,只从受控字段构造过滤条件;不能把客户端任意 JSON 直接当数据库过滤器,允许它自选运算符和访问路径。文档模型与 SQL 的具体防护机制不同,但“由服务端决定查询结构、客户端只能填被允许的值”是共同原则。
四、HTML/JSON 输出:正文仍是正文,不是代码
假设消息正文是文本 "<b>你好</b>"。若产品把它当普通消息文本,浏览器应该展示这些字符本身,而不是把它们解释成页面标记。Go html/template 会对普通不可信字符串按所在模板上下文转义;前端 DOM 写文本时应优先使用 textContent,不能随手塞进 innerHTML。不要把不可信正文转换成 template.HTML 以绕开模板保护。存储原始业务文本与输出时安全编码各司其职;在输入时把全部 < 删除既伤内容,也未必覆盖别的输出位置。
输出位置不同,防护也不同:
| 使用位置 | 风险直觉 | 本章选择 |
|---|---|---|
| HTML 文本节点 | 输入被当标签/脚本 | 普通字符串交给 html/template 的上下文转义,或前端 textContent |
HTML 属性/链接 href | 输入改变属性或使用危险 URL scheme | 属性正确引用/转义,先核对允许的 http/https 等业务协议 |
| 内联脚本、事件属性、任意 HTML | 输入进入可执行上下文 | 不把不可信正文直接放进去;富文本需专门的受控净化流程 |
| JSON API 响应 | 客户端后续可能再渲染 | 正确标 application/json;浏览器显示时仍按目标 DOM 位置处理 |
html/template 的自动转义不是“任何用户 HTML 都安全可渲染”的承诺。若未来允许粗体、链接或富文本编辑,必须有明确允许的标签/属性、经过维护的净化器和最终输出环境校验;聊天内容在 Web、移动端、邮件预览中也可能进入不同上下文。链接本身既可能涉及浏览器 URL 安全,也可能被服务端抓取;后者是第六节的 SSRF 问题,不能只靠 HTML 转义处理。
五、附件:用户文件名不是服务器存储路径
如果未来允许 A 上传附件,客户端提供的 filename、扩展名、Content-Type 和实际文件内容都在不受信边界。像 ../../private.txt 这样的名字不能直接拼到服务器目录;即使用 filepath.Base 截掉路径,也不能解决重名、伪装扩展名、权限与下载映射等全部问题。教学方案为每个上传生成服务器控制的对象键,原始名字只作经过输出处理的展示字段;文件存放在不可被 Web 服务器直接执行的位置,下载经服务端按会话和附件权限检查后提供。
附件类型的检查也要分层:文件名扩展名和请求 Content-Type 都可伪造;按业务只允许必要格式,结合内容签名/解析、容量、数量和处理后的解压/转码上限。被允许的图片也可能有恶意构造或资源耗尽风险,所以解析库、处理隔离和超时仍需审查。若产品有恶意文件扫描/内容转换,它是进一步的处理阶段,不能在本章没有运行服务时写成“附件已安全”。
下载时不要把服务器内部路径交给客户端,也不要因为用户知道一个附件 ID 就跳过资源权限。对不可信或可执行类型,响应的内容类型、Content-Disposition 等行为要与“下载而非执行”政策一致。真正的文件系统权限、对象存储 ACL、扫描队列和保留期属于后续实现与运维边界;本章先把文件名、对象身份、内容类型、存储位置和授权拆开。
六、链接预览:从浏览器链接变成服务器请求后才有 SSRF
用户在 m-a 正文中写一个 URL,只把它作为文本或经校验的浏览器链接展示,服务端未必需要访问该地址。若产品新增“服务端自动抓取链接标题/封面”,用户便能影响服务器向哪里发网络请求;如果服务器可连内部服务、回环地址或云环境元数据地址,就出现 SSRF(服务端请求伪造) 风险。它与浏览器里的 XSS 不同:这里危险动作发生在服务器发出请求时。
先问需求是否真的需要服务端预览。若需要,纸上防护至少包括:只允许业务所需 http/https scheme;限制目标域名/端口,解析 A/AAAA 后阻止回环、私有、链路本地等不允许地址;每次重定向跳转重新验证目标;考虑 DNS 解析在检查与连接之间变化;用出口网络策略、超时、响应体上限和内容类型边界限制损害。仅检查 URL 字符串里“不包含 127.0.0.1”远远不够,域名、重定向和地址编码可绕过这种做法。没有可审查的安全需求与隔离条件时,不在 S3 教学实现中启用自动服务端预览更诚实。
即便预览服务安全地抓取到页面,它生成的标题/摘要仍是不受信输出,回到第四节的 HTML 文本处理;图片下载又回到第五节的类型和大小边界。每经过一个新的解释器,都要重新审查,不能说“URL 入库前过滤过,所以后续都安全”。
七、日志和错误别成为新的泄露出口
排障确实需要证据,但普通访问日志不应保存访问令牌、Session Cookie、数据库连接串、原始 IM 正文、完整附件内容或带秘密参数的 URL。可记录固定路由模板、请求关联标识、处理阶段、有限错误类别、请求/附件大小区间和可审计的授权判定;具体个人/会话身份需最少化并受权限和保留期控制。11.03 已讲低基数指标和受控日志的不同职责;此处不把原始 message_id 当指标标签。
输入中若含换行或控制字符,直接拼成一行自由格式日志可能制造假事件或破坏解析。结构化日志和受控字段能减少风险,但库不会自动识别哪段错误文本包含消息正文或令牌;应用仍要按字段审阅。对外错误使用 09.02 的稳定码和有限说明,不能返回完整 SQL、内部路径、栈、鉴权凭据或服务端可访问 URL。内部日志也要区分“输入不合法”“无权限”“依赖超时”“提交结果未知”,不能为了脱敏把关键证据全部抹掉。
| 负例 | 应在何处阻断或转义 | 仍要核对 |
|---|---|---|
9 字节 "你好呀" | 正文字节上限,写入前拒绝 | 请求体总上限是否另设 |
引号组成的 conversation_id | SQL 值参数化 | u-a 是否有权读目标会话 |
"<b>你好</b>" | HTML 文本上下文转义 | 若作为链接/富文本,另按使用位置处理 |
../../private.txt | 不把原名用作存储路径 | 生成对象键、下载权限与类型 |
| 指向内网的预览 URL | 禁止/限制服务端抓取 | 跳转、DNS、出口及大小上限 |
| 含换行的错误文本 | 结构化受控日志字段 | 不能泄露正文/凭据,仍保留错误类别 |
八、交付数据流防护表与分层练习
学习者交付一张“来源→解析→业务规则→SQL/文档→HTML/JSON/文件/网络/日志”的表,每条边写输入上限、结构限制、输出处理、权限主体与尚未实现的阶段。为正文、附件和 URL 各写一个正常例子和一个负例,再列“服务端已拒绝”“只是浏览器没展示”“结果未知”分别需要的证据。所有示例为静态课程,本章没有运行程序、上传文件或抓取 URL。
分层练习:先答,再展开反馈
1. 为什么一条消息正文没有“通用清洗一次”就够了的规则?
它会进入 SQL、HTML、日志等不同解释器,各自要求不同处理。
2. 请求体、正文和附件上限是同一个数字吗?
不是。整体编码开销、文本字节和文件大小各有独立边界。
3. 只检查 `Content-Length` 足以保证实际体积吗?
不够。真正读取请求体时仍需有界限制。
4. `len("你好")` 在本题 UTF-8 Go 字符串中是多少?
6 字节,不是 2 字节。
5. 前端输入框限制 6 个字符就满足 6 字节上限吗?
不能。字符与 UTF-8 字节数不同,服务端须重新检查。
6. 会话 ID 含引号时,参数化 SQL 会把它当额外 WHERE 条件吗?
不会。它仍是一个数据值。
7. `$1` 可以代替任意客户端提供的 ORDER BY 字段名吗?
不能。SQL 结构由服务端白名单选择。
8. SQL 已参数化,u-c 就可以读 c-a 了吗?
不能。注入防护与对象授权是不同检查。
9. 原样把客户端 JSON 当 MongoDB 查询过滤器合适吗?
不合适。服务端应固定允许的字段和运算结构。
10. 普通消息 `"<b>你好</b>"` 应在 HTML 文本位置怎样显示?
作为文本字符,经上下文转义后展示,不作为可执行/任意页面标记。
11. `template.HTML` 能默认包装不可信正文吗?
不能。它会绕开普通字符串的模板转义保护。
12. 前端写 DOM 文本应优先用 `textContent` 还是 `innerHTML`?
普通正文优先 textContent。
13. URL 被放进 `href` 只做 HTML 转义就够吗?
不够。还要检查允许的 URL scheme 等业务协议和目标属性语境。
14. 用户文件名可直接拼进服务器目录吗?
不能。使用服务器生成对象键,原名仅作受控展示字段。
15. 附件 `Content-Type` 可作为唯一类型证据吗?
不能。它由客户端提供,可伪造,还需业务允许类型与内容检查。
16. 上传大小合格就保证解压/转码不会耗尽资源吗?
不能。处理后的大小、CPU、内存和时间也要设边界。
17. URL 只在页面展示,服务器一定有 SSRF 吗?
不一定。SSRF 风险出现在服务器按用户影响的 URL 主动发请求时。
18. 预览 URL 首跳安全,重定向后可免检吗?
不能。每跳目标和解析地址都需要重新核对。
19. 字符串不含 `127.0.0.1` 就能断定目标不是内网吗?
不能。域名、地址解析、编码、重定向仍可能指向内部目标。
20. 错误日志可以原样保存聊天正文和访问 Token 吗?
不应。用受控字段和有限错误类别,保护凭据与私有内容。
21. 外部错误体应返回完整 SQL 和本地存储路径吗?
不应。返回稳定业务/错误码,内部按访问控制留诊断证据。
22. 哪个检查能同时替代大小、授权、参数化和输出编码?
没有单一检查能替代它们;各自在不同边界承担责任。
来源与下一步
- OWASP:SQL 注入防护与Go:防 SQL 注入:值参数与结构白名单。
- OWASP:XSS 防护与Go
html/template:按输出上下文处理不可信正文。 - OWASP:文件上传、SSRF 防护、日志及Go
MaxBytesReader:文件、网络请求、资源和证据边界。
按学习路线,接下来读09.11 应用测试与交付,把本卷当前接口合同与安全负例整理成可审查交付,再逐步组合 09.12 完整服务;09.09 的异步与长任务在 S5 深化。离开本章前,应能对任意输入指出:谁提供的、在哪个解释器使用、何时限制、输出怎样编码、权限由谁核对。