diff --git a/src/shared/validation/schemas/provider.ts b/src/shared/validation/schemas/provider.ts index bd854cae80..dcf9457217 100644 --- a/src/shared/validation/schemas/provider.ts +++ b/src/shared/validation/schemas/provider.ts @@ -41,10 +41,21 @@ const providerNodeIconUrlSchema = z }) .optional(); +// #6715: the `apiKey` field is reused as the raw `Cookie:` header value for +// cookie-based web providers (Gemini Business, Copilot M365, ChatGPT Web, +// Claude Web, …). Real multi-cookie session headers (many `__Secure-*` entries, +// large session tokens) legitimately exceed the old 10,000-char cap, so saving +// a cookie that the provider's own `validate` check (validateProviderApiKeySchema, +// uncapped) had already accepted failed with HTTP 400 "Too big …<=10000". Raised +// to a still-bounded ceiling — well under the 10 MB default request-body limit and +// the unconstrained SQLite TEXT column — so garbage input is still rejected. +// Same fix shape as #6562 (priority cap raised to 100_000). +export const MAX_PROVIDER_CREDENTIAL_LENGTH = 100_000; + export const createProviderSchema = z .object({ provider: z.string().min(1).max(100), - apiKey: z.string().max(10000).optional(), + apiKey: z.string().max(MAX_PROVIDER_CREDENTIAL_LENGTH).optional(), name: z.string().min(1).max(200), priority: z.number().int().min(1).max(100).optional(), globalPriority: z.number().int().min(1).max(100).nullable().optional(), @@ -91,7 +102,7 @@ export const bulkCreateProviderSchema = z .array( z.object({ name: z.string().min(1).max(200), - apiKey: z.string().min(1).max(10000), + apiKey: z.string().min(1).max(MAX_PROVIDER_CREDENTIAL_LENGTH), // Per-key account id — required for cloudflare-ai (enforced in superRefine below). accountId: z.string().min(1).max(200).optional(), }) @@ -304,7 +315,7 @@ export const updateProviderConnectionSchema = z globalPriority: z.union([z.coerce.number().int().min(1).max(100_000), z.null()]).optional(), defaultModel: z.union([z.string().max(200), z.null()]).optional(), isActive: z.boolean().optional(), - apiKey: z.string().max(10000).optional(), + apiKey: z.string().max(MAX_PROVIDER_CREDENTIAL_LENGTH).optional(), testStatus: z.string().max(50).optional(), lastError: z.union([z.string(), z.null()]).optional(), lastErrorAt: z.union([z.string(), z.null()]).optional(), diff --git a/tests/unit/provider-apikey-cap-6715.test.ts b/tests/unit/provider-apikey-cap-6715.test.ts new file mode 100644 index 0000000000..e831f8963c --- /dev/null +++ b/tests/unit/provider-apikey-cap-6715.test.ts @@ -0,0 +1,65 @@ +// Regression guard for #6715 — the provider connection `apiKey` field was +// hard-capped at 10,000 chars in the Zod save schemas: +// src/shared/validation/schemas/provider.ts +// createProviderSchema (add connection) +// bulkCreateProviderSchema (bulk import) +// updateProviderConnectionSchema (edit connection) +// +// That `apiKey` field is reused as the raw `Cookie:` header value for cookie- +// based web providers (Gemini Business, Copilot M365, ChatGPT Web, Claude Web, +// …). Real multi-cookie session headers (many `__Secure-*` entries, large +// session tokens) legitimately exceed 10,000 chars. The provider's own +// `validate` schema (validateProviderApiKeySchema) has NO cap, so the cookie +// validated as OK — then `save` rejected it with HTTP 400 +// "Too big: expected string to have <=10000 characters". +// +// Fix: raise the ceiling to MAX_PROVIDER_CREDENTIAL_LENGTH (100_000) — still a +// sane anti-abuse bound (well under the 10 MB default request-body limit and +// unconstrained SQLite TEXT storage), just wide enough for real cookie headers. +// Same fix shape as #6562 (priority cap raised to 100_000). +import test from "node:test"; +import assert from "node:assert/strict"; + +const { createProviderSchema, bulkCreateProviderSchema, updateProviderConnectionSchema } = + await import("../../src/shared/validation/schemas.ts"); + +// A realistic large cookie-header value: > 10_000 chars, < the new 100_000 cap. +const LARGE_COOKIE = "__Secure-session=" + "a".repeat(20_000); +// Beyond the new ceiling — must still be rejected (anti-abuse bound preserved). +const OVERSIZE_COOKIE = "x".repeat(100_001); + +test("createProviderSchema accepts a >10000-char cookie apiKey (#6715)", () => { + const result = createProviderSchema.safeParse({ + provider: "gemini-business", + name: "Gemini Business (cookie)", + apiKey: LARGE_COOKIE, + }); + assert.equal(result.success, true, JSON.stringify(result.error?.issues)); +}); + +test("bulkCreateProviderSchema accepts a >10000-char cookie apiKey (#6715)", () => { + const result = bulkCreateProviderSchema.safeParse({ + provider: "gemini-business", + entries: [{ name: "cookie-1", apiKey: LARGE_COOKIE }], + }); + assert.equal(result.success, true, JSON.stringify(result.error?.issues)); +}); + +test("updateProviderConnectionSchema accepts a >10000-char cookie apiKey (#6715)", () => { + const result = updateProviderConnectionSchema.safeParse({ apiKey: LARGE_COOKIE }); + assert.equal(result.success, true, JSON.stringify(result.error?.issues)); +}); + +test("createProviderSchema still rejects an oversize apiKey past the new ceiling (control)", () => { + const result = createProviderSchema.safeParse({ + provider: "gemini-business", + name: "too big", + apiKey: OVERSIZE_COOKIE, + }); + assert.equal(result.success, false); +}); + +test("updateProviderConnectionSchema still rejects an oversize apiKey past the new ceiling (control)", () => { + const result = updateProviderConnectionSchema.safeParse({ apiKey: OVERSIZE_COOKIE }); + assert.equal(result.success, false); +});