Files
sub2api/backend/internal/service/gemini_upstream_url_test.go
T
shaw 017f6bbd5e fix(gateway): 收紧上游 URL 路径片段校验
网关有若干位置会把客户端可控的字符串拼进上游请求的 URL path(Responses
子路径、Gemini 模型名)。此前这些字符串未经校验直接参与拼接,可能改变上游
请求的路径结构,使实际发出的请求与客户端意图不一致。

- 新增 internal/service/upstream_path_guard.go:路径片段闭集允许清单
  (\w + `-` + `.`),拒绝空片段、纯点片段、超长片段与过深后缀
- /responses/*subpath 三条路由入口新增守卫,不可转发的子路径直接 404;
  service 层同时保证不产出不合规后缀,拼接函数再兜底一层
- Gemini AI Studio 原先 5 处重复的 URL 拼接收敛为唯一构造点
  buildGeminiAIStudioModelActionURL(校验模型片段 + action 白名单)
- Gemini native / GetModel handler 增加入口校验;ForwardAIStudioGET 逐片段校验
- Grok video 端点的 request_id 增加片段合规校验

合法子路径(/compact、/compact/detail、/{id}/cancel 形态)与既有模型名行为
不变,通配路由保留。
2026-07-31 16:11:45 +08:00

63 lines
2.0 KiB
Go

package service
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestBuildGeminiAIStudioModelActionURL(t *testing.T) {
const base = "https://generativelanguage.googleapis.com"
got, err := buildGeminiAIStudioModelActionURL(base, "gemini-2.5-pro", "generateContent", false)
require.NoError(t, err)
require.Equal(t, base+"/v1beta/models/gemini-2.5-pro:generateContent", got)
got, err = buildGeminiAIStudioModelActionURL(base+"/", " gemini-2.5-flash ", "streamGenerateContent", true)
require.NoError(t, err)
require.Equal(t, base+"/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse", got)
got, err = buildGeminiAIStudioModelActionURL(base, "gemini-2.5-pro", "countTokens", false)
require.NoError(t, err)
require.Equal(t, base+"/v1beta/models/gemini-2.5-pro:countTokens", got)
}
// TestBuildGeminiAIStudioModelActionURLRejectsNonConformingModel 锁定不变式:
// 模型名来自客户端(native 路由的 URL 片段 / compat 路由的请求体),
// 只有合规的路径片段才允许拼进上游 URL。
func TestBuildGeminiAIStudioModelActionURLRejectsNonConformingModel(t *testing.T) {
const base = "https://generativelanguage.googleapis.com"
for _, model := range []string{
"../../x/y",
"..",
".",
"gemini-2.5-pro/../../x",
`..\..\x`,
"gemini-2.5-pro?a=b",
"gemini-2.5-pro#frag",
"gemini-2.5-pro%2f..",
"gemini 2.5 pro",
"gemini\x00pro",
"gemini-2.5-pro@001",
"gemini~pro",
"models/gemini-2.5-pro",
"...",
"",
" ",
} {
t.Run("model_"+model, func(t *testing.T) {
_, err := buildGeminiAIStudioModelActionURL(base, model, "generateContent", false)
require.Error(t, err, "model %q must be rejected", model)
require.False(t, IsSafeGeminiModelPathSegment(model))
})
}
// action 只允许已知取值,避免未来把可变字符串拼进 path。
_, err := buildGeminiAIStudioModelActionURL(base, "gemini-2.5-pro", "deleteModel", false)
require.Error(t, err)
_, err = buildGeminiAIStudioModelActionURL("", "gemini-2.5-pro", "generateContent", false)
require.Error(t, err)
}