From 0baabcdc4bc211b5900f1709108ed428c4ac04d2 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 19 Jul 2026 16:17:09 +0000 Subject: [PATCH] fix(ai): address Alibaba image audit findings --- packages/ai/src/protocols/alibaba-images.ts | 36 +++++----- packages/ai/test/auth-options.types.ts | 7 ++ .../generates-with-qwen-image-2-0.json | 4 +- .../generates-with-wan-2-7.json | 4 +- .../provider/alibaba-images.recorded.test.ts | 70 ++++++++++++++++++- .../ai/test/provider/alibaba-images.test.ts | 51 ++++++++++++++ 6 files changed, 149 insertions(+), 23 deletions(-) diff --git a/packages/ai/src/protocols/alibaba-images.ts b/packages/ai/src/protocols/alibaba-images.ts index 45282a014b..23ae5d8b61 100644 --- a/packages/ai/src/protocols/alibaba-images.ts +++ b/packages/ai/src/protocols/alibaba-images.ts @@ -25,7 +25,7 @@ export const PATH = "/services/aigc/multimodal-generation/generation" export type Family = "qwen" | "wan" -export interface QwenImageOptions extends Record { +export interface QwenImageOptions { readonly negativePrompt?: string readonly promptExtend?: boolean readonly watermark?: boolean @@ -36,14 +36,14 @@ export interface WanColor { readonly ratio: string } -export interface WanImageOptions extends Record { +export interface WanImageOptions { readonly resolution?: "1K" | "2K" | "4K" readonly thinkingMode?: boolean readonly colorPalette?: ReadonlyArray readonly watermark?: boolean } -export interface AlibabaImageOptions extends Record { +export interface AlibabaImageOptions { readonly qwen?: QwenImageOptions readonly wan?: WanImageOptions } @@ -240,7 +240,8 @@ const expiration = (url: string) => { if (!URL.canParse(url)) return undefined const value = new URL(url).searchParams.get("Expires") if (value === null || !Number.isFinite(Number(value))) return undefined - return new Date(Number(value) * 1000).toISOString() + const date = new Date(Number(value) * 1000) + return Number.isNaN(date.getTime()) ? undefined : date.toISOString() } export const model = (input: ModelInput) => { @@ -297,20 +298,21 @@ export const model = (input: ModelInput) => { return yield* invalidOutput("Alibaba Images returned no images", { requestId: decoded.request_id }) return new ImageResponse({ - images: urls.map( - (url) => - new GeneratedImage({ - mediaType: "image/png", - data: url, - providerMetadata: { - alibaba: { - modelId: request.model.id, - family: input.family, - expiresAt: expiration(url), - }, + images: urls.map((url) => { + const expiresAt = expiration(url) + return new GeneratedImage({ + mediaType: "image/png", + data: url, + expiresAt, + providerMetadata: { + alibaba: { + modelId: request.model.id, + family: input.family, + expiresAt, }, - }), - ), + }, + }) + }), usage: decoded.usage === undefined ? undefined diff --git a/packages/ai/test/auth-options.types.ts b/packages/ai/test/auth-options.types.ts index c430106789..2a7e9626c9 100644 --- a/packages/ai/test/auth-options.types.ts +++ b/packages/ai/test/auth-options.types.ts @@ -2,6 +2,7 @@ import { Config } from "effect" import { Auth } from "../src/route" import type { ModelFactory } from "../src/route/auth-options" import * as OpenAIChat from "../src/protocols/openai-chat" +import * as Alibaba from "../src/providers/alibaba" import * as AmazonBedrock from "../src/providers/amazon-bedrock" import * as Anthropic from "../src/providers/anthropic" import * as AnthropicCompatible from "../src/providers/anthropic-compatible" @@ -250,3 +251,9 @@ Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key GitHubCopilot.configure({ baseURL: "https://copilot.test", apiKey: "copilot-key" }).model("gpt-4.1") // @ts-expect-error GitHub Copilot model selectors only accept model ids. GitHubCopilot.configure({ baseURL: "https://copilot.test", apiKey: "copilot-key" }).model("gpt-4.1", {}) + +Alibaba.configure({ image: { providerOptions: { qwen: { promptExtend: true } } } }).image("qwen-image-2.0") +// @ts-expect-error Alibaba Qwen options are closed and reject unimplemented parameters. +Alibaba.configure({ image: { providerOptions: { qwen: { style: "photorealistic" } } } }) +// @ts-expect-error Alibaba Wan options are closed and reject unimplemented parameters. +Alibaba.configure({ image: { providerOptions: { wan: { parameters: { watermark: true } } } } }) diff --git a/packages/ai/test/fixtures/recordings/alibaba-images/generates-with-qwen-image-2-0.json b/packages/ai/test/fixtures/recordings/alibaba-images/generates-with-qwen-image-2-0.json index 8c051c7532..648688aa17 100644 --- a/packages/ai/test/fixtures/recordings/alibaba-images/generates-with-qwen-image-2-0.json +++ b/packages/ai/test/fixtures/recordings/alibaba-images/generates-with-qwen-image-2-0.json @@ -3,7 +3,7 @@ "metadata": { "tags": ["prefix:alibaba-images", "provider:alibaba", "protocol:alibaba-images"], "name": "alibaba-images/generates-with-qwen-image-2-0", - "recordedAt": "2026-07-19T16:05:19.642Z" + "recordedAt": "2026-07-19T16:15:36.863Z" }, "interactions": [ { @@ -21,7 +21,7 @@ "headers": { "content-type": "application/json" }, - "body": "{\"output\":{\"choices\":[{\"finish_reason\":\"stop\",\"message\":{\"content\":[{\"image\":\"https://dashscope-463f.oss-accelerate.aliyuncs.com/7d/7b/20260720/991d10e3/fe425a64-dc6f-4c67-a4fa-a06810984ba9.png?Expires=1785082919&OSSAccessKeyId=fixture&Signature=fixture\"}],\"role\":\"assistant\"}}]},\"usage\":{\"height\":512,\"image_count\":1,\"width\":512},\"request_id\":\"484490f4-9881-90eb-8d52-d270d2395920\"}" + "body": "{\"output\":{\"choices\":[{\"finish_reason\":\"stop\",\"message\":{\"content\":[{\"image\":\"https://dashscope-463f.oss-accelerate.aliyuncs.com/7d/04/20260720/991d10e3/93325a03-aa95-4dd3-a395-8d7246b5e72c.png?Expires=1785083536&OSSAccessKeyId=%5BREDACTED%5D&Signature=%5BREDACTED%5D\"}],\"role\":\"assistant\"}}]},\"usage\":{\"height\":512,\"image_count\":1,\"width\":512},\"request_id\":\"9f94d544-393b-9d6f-b005-4d0ff21f6fdf\"}" } } ] diff --git a/packages/ai/test/fixtures/recordings/alibaba-images/generates-with-wan-2-7.json b/packages/ai/test/fixtures/recordings/alibaba-images/generates-with-wan-2-7.json index 373482ec13..37cf28a9f9 100644 --- a/packages/ai/test/fixtures/recordings/alibaba-images/generates-with-wan-2-7.json +++ b/packages/ai/test/fixtures/recordings/alibaba-images/generates-with-wan-2-7.json @@ -3,7 +3,7 @@ "metadata": { "tags": ["prefix:alibaba-images", "provider:alibaba", "protocol:alibaba-images"], "name": "alibaba-images/generates-with-wan-2-7", - "recordedAt": "2026-07-19T16:05:23.617Z" + "recordedAt": "2026-07-19T16:15:40.689Z" }, "interactions": [ { @@ -21,7 +21,7 @@ "headers": { "content-type": "application/json" }, - "body": "{\"output\":{\"choices\":[{\"finish_reason\":\"stop\",\"message\":{\"content\":[{\"image\":\"https://dashscope-463f.oss-accelerate.aliyuncs.com/1d/ae/20260720/10739b93/1784477122_595381b5.png?Expires=1784563522&OSSAccessKeyId=fixture&Signature=fixture\",\"type\":\"image\"}],\"role\":\"assistant\"}}],\"finished\":true},\"usage\":{\"image_count\":1,\"input_tokens\":31,\"output_tokens\":2,\"size\":\"1024*1024\",\"total_tokens\":33},\"request_id\":\"ec5cacb5-c7b0-9e46-8459-49003a3816c3\"}" + "body": "{\"output\":{\"choices\":[{\"finish_reason\":\"stop\",\"message\":{\"content\":[{\"image\":\"https://dashscope-463f.oss-accelerate.aliyuncs.com/1d/0b/20260720/10739b93/1784477739_553aee1e.png?Expires=1784564140&OSSAccessKeyId=%5BREDACTED%5D&Signature=%5BREDACTED%5D\",\"type\":\"image\"}],\"role\":\"assistant\"}}],\"finished\":true},\"usage\":{\"image_count\":1,\"input_tokens\":31,\"output_tokens\":2,\"size\":\"1024*1024\",\"total_tokens\":33},\"request_id\":\"3eb93323-2d8f-9aa5-b010-82dd8fab80ea\"}" } } ] diff --git a/packages/ai/test/provider/alibaba-images.recorded.test.ts b/packages/ai/test/provider/alibaba-images.recorded.test.ts index 4980cd81b1..1058cc6554 100644 --- a/packages/ai/test/provider/alibaba-images.recorded.test.ts +++ b/packages/ai/test/provider/alibaba-images.recorded.test.ts @@ -1,5 +1,5 @@ -import { describe, expect } from "bun:test" -import { Effect } from "effect" +import { describe, expect, test } from "bun:test" +import { Effect, Option, Schema } from "effect" import { Image } from "../../src" import { Alibaba } from "../../src/providers" import { recordedTests } from "../recorded-test" @@ -8,11 +8,77 @@ const alibaba = Alibaba.configure({ apiKey: process.env.DASHSCOPE_API_KEY ?? "fixture", }) +const SIGNED_QUERY_CREDENTIALS = new Set( + [ + "AccessKeyId", + "AWSAccessKeyId", + "GoogleAccessId", + "OSSAccessKeyId", + "Signature", + "SecurityToken", + "X-Amz-Credential", + "X-Amz-Security-Token", + "X-Amz-Signature", + "X-Goog-Credential", + "X-Goog-Signature", + "X-OSS-Security-Token", + ].map((name) => name.replace(/[^a-z0-9]/gi, "").toLowerCase()), +) + +const redactSignedUrl = (value: string) => { + if (!URL.canParse(value)) return value + const url = new URL(value) + if (url.protocol !== "http:" && url.protocol !== "https:") return value + url.searchParams.forEach((_, name) => { + if (SIGNED_QUERY_CREDENTIALS.has(name.replace(/[^a-z0-9]/gi, "").toLowerCase())) + url.searchParams.set(name, "[REDACTED]") + }) + return url.toString() +} + +const redactSignedUrls = (body: string) => + Option.match(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(body), { + onNone: () => body, + onSome: (value) => + JSON.stringify( + (function redact(input: unknown): unknown { + if (typeof input === "string") return redactSignedUrl(input) + if (Array.isArray(input)) return input.map(redact) + if (input === null || typeof input !== "object") return input + return Object.fromEntries(Object.entries(input).map(([key, child]) => [key, redact(child)])) + })(value), + ), + }) + const recorded = recordedTests({ prefix: "alibaba-images", provider: "alibaba", protocol: "alibaba-images", requires: ["DASHSCOPE_API_KEY"], + options: { redact: { body: redactSignedUrls } }, +}) + +test("recorder redacts signed URL credentials in nested JSON strings repeatably", () => { + const body = JSON.stringify({ + output: { + image: + "https://example.oss.aliyuncs.com/image.png?Expires=1893456000&OSSAccessKeyId=access&Signature=signature&x-oss-security-token=token", + }, + mirrors: ["https://storage.googleapis.com/image.png?X-Goog-Credential=credential&X-Goog-Signature=signature"], + }) + const once = redactSignedUrls(body) + const twice = redactSignedUrls(once) + const parsed = JSON.parse(once) + const oss = new URL(parsed.output.image) + const gcs = new URL(parsed.mirrors[0]) + + expect(oss.searchParams.get("Expires")).toBe("1893456000") + expect(oss.searchParams.get("OSSAccessKeyId")).toBe("[REDACTED]") + expect(oss.searchParams.get("Signature")).toBe("[REDACTED]") + expect(oss.searchParams.get("x-oss-security-token")).toBe("[REDACTED]") + expect(gcs.searchParams.get("X-Goog-Credential")).toBe("[REDACTED]") + expect(gcs.searchParams.get("X-Goog-Signature")).toBe("[REDACTED]") + expect(twice).toBe(once) }) describe("Alibaba Images recorded", () => { diff --git a/packages/ai/test/provider/alibaba-images.test.ts b/packages/ai/test/provider/alibaba-images.test.ts index d8bfd12b93..b08da74970 100644 --- a/packages/ai/test/provider/alibaba-images.test.ts +++ b/packages/ai/test/provider/alibaba-images.test.ts @@ -50,6 +50,7 @@ describe("Alibaba Images", () => { }) expect(result.image?.data).toContain("dashscope-result-intl") + expect(result.image?.expiresAt).toBe("2030-01-01T00:00:00.000Z") expect(result.image?.providerMetadata).toEqual({ alibaba: { modelId: "qwen-image-2.0-pro", @@ -193,4 +194,54 @@ describe("Alibaba Images", () => { ), ), ) + + it.effect("rejects parameters overlays owned by the protocol", () => + Image.generate({ + model: Alibaba.configure({ apiKey: "test" }).image("qwen-image-2.0"), + prompt: "A robot", + http: { body: { parameters: { watermark: true } } }, + }).pipe( + Effect.flip, + Effect.tap((error) => + Effect.sync(() => { + expect(error.reason._tag).toBe("InvalidRequest") + expect(error.message).toContain("http.body cannot overlay protocol-owned field(s): parameters") + }), + ), + Effect.provide( + ImageClient.layer.pipe( + Layer.provide( + dynamicResponse(() => Effect.die("reserved body validation must happen before the request is sent")), + ), + ), + ), + ), + ) + + it.effect("ignores finite expiration values outside the Date range", () => + Effect.gen(function* () { + const result = yield* Image.generate({ + model: Alibaba.configure({ apiKey: "test" }).image("qwen-image-2.0"), + prompt: "A robot", + }) + + expect(result.image?.expiresAt).toBeUndefined() + expect(result.image?.providerMetadata?.alibaba?.expiresAt).toBeUndefined() + }).pipe( + Effect.provide( + ImageClient.layer.pipe( + Layer.provide( + dynamicResponse((input) => { + const payload = response("qwen") + payload.output.choices[0].message.content[0].image = + "https://dashscope-result-intl.oss-cn-singapore.aliyuncs.com/result.png?Expires=9007199254740991" + return Effect.succeed( + input.respond(JSON.stringify(payload), { headers: { "content-type": "application/json" } }), + ) + }), + ), + ), + ), + ), + ) })