| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495 |
- 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)
- }
|