- SSRF 三层:① 字面主机判定(**链路本地** `169.254.0.0/16`/`fe80::/10`(先 Unmap ⇒ `::ffff:169.254.169.254` 绕不过)+ 云元数据 `100.100.100.200`、`fd00:ec2::/64`);② 重定向逐跳校验 + 上限 5 跳; ③ **建连前校验解析结果并用校验过的 IP 直连**(防 DNS 重绑,拨号器不再二次解析) - **没打断同源自取**(两条独立证据):`127.0.0.1:<port>/api/v1/files/<32hex>.png` 端到端取回成功、 私网地址**原样交给底层拨号器**;被拒地址则底层拨号器 0 次调用(无真实连接发出) - 失败文案**不含 URL/查询串签名**;非图片响应仍 `ErrNotImage` 且不泄漏响应体 - base64 路径长度预检(**先于 DecodeString 与 `data:` 短路**,零分配):口径与网关同值同推导 (`((5MiB+2)/3)*4+64`),超限 `ErrImageTooLarge`、文案只含上限与本次长度 - 复核:vision 包 build/vet/测试绿(15 条新用例);**4 条变异反证**(守卫整体短路 → 8 条被拒地址全中; 只短路链路本地分支 → 4 红且实测「无守卫时进程真的去连 169.254.169.254」;预检恒 nil → 3 红; 去掉重定向校验仍全绿 ⇒ 证明第三层独立兜住) - 登记:`CHAT_IMAGE_MAX_BYTES` 调大 > 默认时「网关照放、vision 拒」的配置耦合(待单一真源对齐)
42 lines
2.3 KiB
Go
42 lines
2.3 KiB
Go
package vision
|
|
|
|
import "fmt"
|
|
|
|
// 内联图像(base64 / data: URI)的体积预算:与网关 REST 入口
|
|
// internal/services/gateway/rest.go 的图片上限同口径 —— 5 MiB 解码后字节,
|
|
// 外加 64 字符的 data: 前缀余量。网关是**入口层**实现,这里是**解码层**的
|
|
// 同一口径兜底:REST 入口已有 5 MiB 上限,但任何未来新入口,或把 WS 上限
|
|
// 放宽,都会重新打开「先按约 4/3 分配再报错」这个洞(G3 的纵深)。
|
|
//
|
|
// 口径换算只有一处:(n+2)/3*4 是 base64 每 3 字节编码为 4 字符的膨胀系数
|
|
// (余 1、2 字节时末组仍需补满 4 字符),再加 data: 前缀余量 —— 恰好
|
|
// 5 MiB 的合法图片编码后正好等于该上限,不会被误拒。
|
|
//
|
|
// 注意生效范围:网关的生效上限可由配置 CHAT_IMAGE_MAX_BYTES 调整(默认
|
|
// 5242880 与本常量同值),本包读不到网关配置,因此这里钉住的是**默认口径的
|
|
// 硬顶** —— 解码层不随入口配置放宽;入口层比它更严时以入口层为准。
|
|
const (
|
|
// MaxInlineImageBytes 是一张内联图像解码后的字节上限(5 MiB),
|
|
// 与网关 POST /api/v1/upload 的 maxUploadBytes 同口径。
|
|
MaxInlineImageBytes = 5 << 20
|
|
// InlineImagePrefixAllowance 是 data: URI 前缀(如
|
|
// "data:image/png;base64,")所占的字符余量,不含 base64 载荷本身。
|
|
InlineImagePrefixAllowance = 64
|
|
// MaxInlineImageChars 是同一口径换算到 base64 字符串长度的上限。
|
|
MaxInlineImageChars = ((MaxInlineImageBytes+2)/3)*4 + InlineImagePrefixAllowance
|
|
)
|
|
|
|
// checkInlineImageLength 对 base64 / data: URI 字符串做长度预检,在任何
|
|
// 解码或短路之前拦下超限载荷:解码要先按约 4/3 分配,不预检就是「先分配
|
|
// 再报错」,放宽了入口上限的新入口会因此被放大成内存压力。判定按**字符串
|
|
// 长度**(零分配),与网关入口 checkChatImageSize 同一口径。
|
|
//
|
|
// 错误文案只带上限与本次长度,绝不包含图片内容。
|
|
func checkInlineImageLength(src string) error {
|
|
if len(src) <= MaxInlineImageChars {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("vision: %w: 内联图像上限 %d 字符(解码后约 %d MiB),本次 %d 字符",
|
|
ErrImageTooLarge, MaxInlineImageChars, MaxInlineImageBytes>>20, len(src))
|
|
}
|