Merge pull request #3794 from fengshao1227/fix/image-gen-namespace-permission-bypass

fix(image): 识别 Codex namespace image_gen 工具声明以拦截生图请求
This commit is contained in:
Wesley Liddick
2026-07-07 20:42:43 +08:00
committed by GitHub
3 changed files with 153 additions and 2 deletions
@@ -39,6 +39,9 @@ func IsImageGenerationIntent(endpoint string, requestedModel string, body []byte
if openAIJSONToolsContainImageGeneration(gjson.GetBytes(body, "tools")) {
return true
}
if openAIJSONInputContainsImageGenTool(gjson.GetBytes(body, "input")) {
return true
}
return openAIJSONToolChoiceSelectsImageGeneration(gjson.GetBytes(body, "tool_choice"))
}
@@ -94,11 +97,52 @@ func openAIJSONToolsContainImageGeneration(tools gjson.Result) bool {
found = true
return false
}
if isImageGenNamespaceTool(item) {
found = true
return false
}
return true
})
return found
}
// isImageGenNamespaceTool detects the Codex namespace-style image generation
// tool declaration: { "type": "namespace", "name": "image_gen", ... }.
// Codex /image uses this instead of the flat { "type": "image_generation" }.
func isImageGenNamespaceTool(tool gjson.Result) bool {
return openAIJSONString(tool.Get("type")) == "namespace" &&
openAIJSONString(tool.Get("name")) == "image_gen"
}
// openAIJSONInputContainsImageGenTool scans Responses input items for
// additional_tools entries that declare the image_gen namespace. This covers
// the "Responses Lite" format where tools are embedded inside input items
// rather than top-level tools.
func openAIJSONInputContainsImageGenTool(input gjson.Result) bool {
if !input.IsArray() {
return false
}
found := false
input.ForEach(func(_, item gjson.Result) bool {
if openAIJSONString(item.Get("type")) != "additional_tools" {
return true
}
tools := item.Get("tools")
if !tools.IsArray() {
return true
}
tools.ForEach(func(_, tool gjson.Result) bool {
if isImageGenNamespaceTool(tool) {
found = true
return false
}
return true
})
return !found
})
return found
}
func openAIRequestBodyHasImageGenerationTool(body []byte) bool {
if len(body) == 0 || !gjson.ValidBytes(body) {
return false
@@ -55,6 +55,27 @@ func TestIsImageGenerationIntent(t *testing.T) {
body: []byte(`{"model":"gpt-5.4","input":"write code"}`),
want: false,
},
{
name: "namespace image_gen tool in top-level tools",
endpoint: "/v1/responses",
model: "gpt-5.5",
body: []byte(`{"model":"gpt-5.5","tools":[{"type":"namespace","name":"image_gen","tools":[{"type":"function","name":"imagegen"}]}]}`),
want: true,
},
{
name: "namespace image_gen in input additional_tools (Responses Lite)",
endpoint: "/v1/responses",
model: "gpt-5.5",
body: []byte(`{"model":"gpt-5.5","input":[{"type":"additional_tools","role":"developer","tools":[{"type":"namespace","name":"image_gen","tools":[{"type":"function","name":"imagegen"}]}]}]}`),
want: true,
},
{
name: "non-image namespace tool is not flagged",
endpoint: "/v1/responses",
model: "gpt-5.5",
body: []byte(`{"model":"gpt-5.5","tools":[{"type":"namespace","name":"code_tools","tools":[{"type":"function","name":"run"}]}]}`),
want: false,
},
}
for _, tt := range tests {
@@ -64,6 +85,58 @@ func TestIsImageGenerationIntent(t *testing.T) {
}
}
func TestIsImageGenerationIntentMap_NamespaceImageGen(t *testing.T) {
tests := []struct {
name string
reqBody map[string]any
want bool
}{
{
name: "top-level namespace image_gen",
reqBody: map[string]any{
"model": "gpt-5.5",
"tools": []any{
map[string]any{"type": "namespace", "name": "image_gen", "tools": []any{
map[string]any{"type": "function", "name": "imagegen"},
}},
},
},
want: true,
},
{
name: "additional_tools in input",
reqBody: map[string]any{
"model": "gpt-5.5",
"input": []any{
map[string]any{
"type": "additional_tools",
"tools": []any{
map[string]any{"type": "namespace", "name": "image_gen"},
},
},
},
},
want: true,
},
{
name: "non-image namespace not flagged",
reqBody: map[string]any{
"model": "gpt-5.5",
"tools": []any{
map[string]any{"type": "namespace", "name": "code_tools"},
},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, IsImageGenerationIntentMap("/v1/responses", "gpt-5.5", tt.reqBody))
})
}
}
func TestResolveOpenAIResponsesImageBillingConfigUsesCurrentBodyModel(t *testing.T) {
imageModel, imageSize, err := resolveOpenAIResponsesImageBillingConfigFromBody(
[]byte(`{"model":"mapped-image-model","tools":[{"type":"image_generation","size":"1024x1024"}]}`),
@@ -593,8 +593,14 @@ func isCodexSparkModel(model string) bool {
}
func hasOpenAIImageGenerationTool(reqBody map[string]any) bool {
rawTools, ok := reqBody["tools"]
if !ok || rawTools == nil {
if toolsContainImageGeneration(reqBody["tools"]) {
return true
}
return inputContainsImageGenNamespace(reqBody["input"])
}
func toolsContainImageGeneration(rawTools any) bool {
if rawTools == nil {
return false
}
tools, ok := rawTools.([]any)
@@ -609,6 +615,34 @@ func hasOpenAIImageGenerationTool(reqBody map[string]any) bool {
if strings.TrimSpace(firstNonEmptyString(toolMap["type"])) == "image_generation" {
return true
}
if isImageGenNamespaceToolMap(toolMap) {
return true
}
}
return false
}
func isImageGenNamespaceToolMap(tool map[string]any) bool {
return strings.TrimSpace(firstNonEmptyString(tool["type"])) == "namespace" &&
strings.TrimSpace(firstNonEmptyString(tool["name"])) == "image_gen"
}
func inputContainsImageGenNamespace(rawInput any) bool {
input, ok := rawInput.([]any)
if !ok {
return false
}
for _, rawItem := range input {
item, ok := rawItem.(map[string]any)
if !ok {
continue
}
if strings.TrimSpace(firstNonEmptyString(item["type"])) != "additional_tools" {
continue
}
if toolsContainImageGeneration(item["tools"]) {
return true
}
}
return false
}