From fe21f352f6a35db7451bb569eb2441f6a32308c1 Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Thu, 30 Jul 2026 09:36:54 +0800 Subject: [PATCH] fix(image): validate caire settings as integers (#680) caire's -width/-height/-blur/-sobel flags are integer-only; a schema-valid float crashed the binary and surfaced as a corrupt-file 422. Reject fractional values at validation time with a settings-shaped 400. Fixes #672. --- .../src/routes/tools/content-aware-resize.ts | 10 +++++---- .../tools/image/content-aware-resize.test.ts | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/api/src/routes/tools/content-aware-resize.ts b/apps/api/src/routes/tools/content-aware-resize.ts index 9de77096..7789926e 100644 --- a/apps/api/src/routes/tools/content-aware-resize.ts +++ b/apps/api/src/routes/tools/content-aware-resize.ts @@ -16,12 +16,14 @@ import { InputValidationError } from "../../modality/contract.js"; import { inputHandlerFor } from "../../modality/input-handler.js"; import { registerToolProcessFn } from "../tool-factory.js"; +// caire's -width/-height/-blur/-sobel flags are integer-only; a fractional +// value makes the binary exit with a flag parse error. const settingsSchema = z.object({ - width: z.number().positive().optional(), - height: z.number().positive().optional(), + width: z.number().int().positive().optional(), + height: z.number().int().positive().optional(), protectFaces: z.boolean().default(false), - blurRadius: z.number().min(0).max(20).default(4), - sobelThreshold: z.number().min(1).max(20).default(2), + blurRadius: z.number().int().min(0).max(20).default(4), + sobelThreshold: z.number().int().min(1).max(20).default(2), square: z.boolean().default(false), }); diff --git a/tests/integration/tools/image/content-aware-resize.test.ts b/tests/integration/tools/image/content-aware-resize.test.ts index c04a7bd4..d810213c 100644 --- a/tests/integration/tools/image/content-aware-resize.test.ts +++ b/tests/integration/tools/image/content-aware-resize.test.ts @@ -57,6 +57,27 @@ describe("Content-Aware Resize", () => { expect([200, 422]).toContain(res.statusCode); }, 60_000); + it("rejects fractional values for caire's integer-only flags", async () => { + for (const settings of [ + { width: 100.5 }, + { height: 80.25 }, + { blurRadius: 2.5 }, + { sobelThreshold: 10.5 }, + ]) { + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, + { name: "settings", content: JSON.stringify(settings) }, + ]); + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/image/content-aware-resize", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + expect(res.statusCode, JSON.stringify(settings)).toBe(400); + } + }, 60_000); + it("rejects requests without a file", async () => { const { body, contentType } = createMultipartPayload([ { name: "settings", content: JSON.stringify({ width: 150 }) },