Files
sub2api/backend/internal/service/upstream_path_guard.go
T

96 lines
3.5 KiB
Go
Raw Normal View History

package service
import (
"fmt"
"strings"
)
// 上游 URL 路径片段护栏。
//
// 网关会把若干客户端可控的字符串(Responses 子路径、Gemini 模型名)拼进上游请求
// 的 URL path。约定:这些字符串只允许"结构惰性"的字符,即拼进去之后不可能改变
// 上游请求的路径结构;不符合的一律拒绝。
//
// 实现上是**闭集允许清单(默认拒绝)**,请勿改成"逐个拒绝已知的坏字符":后者要求
// 穷举,漏一项就失效;默认拒绝则相反,清单外的写法天然被挡住,无需跟着改代码。
//
// 另外注意:到达业务代码的 c.Request.URL.Path 已经是百分号解码后的结果,因此校验
// 必须放在这一层,不能假设上游看到的路径与客户端书写形式一致。
//
// 这里只校验、不改写:把不合规输入自动修正成合规路径,会让上游收到与客户端意图
// 不同的请求,也会掩盖调用方的错误。
const (
// maxUpstreamPathSegmentLen 单个路径片段长度上限。真实的 response id、模型名
// 都远短于此,留足余量只为拒绝异常输入。
maxUpstreamPathSegmentLen = 128
// maxUpstreamPathSegments 后缀允许的片段数上限(如 /{id}/cancel 为 2)。
maxUpstreamPathSegments = 8
)
// isSafeUpstreamPathSegmentByte 是闭集允许清单:只放行 `\w`(即 [A-Za-z0-9_]
// 以及真实取值必需的 `-` 与 `.`。其余字符(含控制字符与非 ASCII)一律拒绝。
func isSafeUpstreamPathSegmentByte(b byte) bool {
switch {
case b >= 'a' && b <= 'z', b >= 'A' && b <= 'Z', b >= '0' && b <= '9':
return true
case b == '_', b == '-', b == '.':
return true
default:
return false
}
}
// isSafeUpstreamPathSegment 判断 segment 能否原样拼进上游 URL 的一个 path 片段。
//
// 允许清单里唯一在路径语义中有特殊含义的字符是 `.`,因此额外要求片段不能只由点
// 组成——各类实现对这种片段的解释并不一致,直接拒绝最省心。
func isSafeUpstreamPathSegment(segment string) bool {
if segment == "" || len(segment) > maxUpstreamPathSegmentLen {
return false
}
dotsOnly := true
for i := 0; i < len(segment); i++ {
if !isSafeUpstreamPathSegmentByte(segment[i]) {
return false
}
if segment[i] != '.' {
dotsOnly = false
}
}
return !dotsOnly
}
// sanitizedUpstreamPathSuffix 校验 "/a/b" 形态的路径后缀。
// ok=false 表示后缀不可转发,调用方必须拒绝请求,而不是降级成空后缀——否则
// /responses/compact 之类的请求语义会被静默改写。空后缀合法,表示"没有子路径"。
func sanitizedUpstreamPathSuffix(raw string) (string, bool) {
suffix := strings.TrimSpace(raw)
if suffix == "" {
return "", true
}
if !strings.HasPrefix(suffix, "/") {
return "", false
}
segments := strings.Split(strings.TrimPrefix(suffix, "/"), "/")
if len(segments) > maxUpstreamPathSegments {
return "", false
}
for _, segment := range segments {
if !isSafeUpstreamPathSegment(segment) {
return "", false
}
}
return suffix, true
}
// validateUpstreamPathSegment 供 URL 构造点使用:不合规的片段直接变成显式错误,
// 不再继续构造与发出上游请求。
func validateUpstreamPathSegment(kind, segment string) error {
if isSafeUpstreamPathSegment(strings.TrimSpace(segment)) {
return nil
}
// 不回显原始输入,避免把它写进日志与错误响应。
return fmt.Errorf("invalid %s for upstream url path", kind)
}