Restore the community photo after reload and reject empty, oversized, SVG, and spoofed uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-10 15:50:32 -06:00
co-authored by Cursor
parent 6ccc1e8c8e
commit 234f3998ad
18 changed files with 1072 additions and 112 deletions
@@ -0,0 +1,164 @@
import { describe, expect, it, vi } from "vitest";
import {
CreateFlowUploadValidationError,
maxBytesForPurpose,
validateCreateFlowUploadBytes,
validateCreateFlowUploadFile,
} from "../../lib/create/createFlowUploadValidation";
import { uploadCreateFlowFile } from "../../lib/create/uploadToServer";
/** 1×1 PNG (valid IHDR). */
const PNG_1X1 = Uint8Array.from(
Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
"base64",
),
);
/** SOF0 1×1 JPEG (header-decodable; not necessarily a complete scan). */
const JPEG_1X1_SOF = Uint8Array.from([
0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01,
0x11, 0x00, 0xff, 0xd9,
]);
const GIF_1X1 = Uint8Array.from([
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x3b,
]);
/** VP8X 1×1 WebP. */
const WEBP_1X1 = Uint8Array.from([
0x52, 0x49, 0x46, 0x46, 0x16, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56,
0x50, 0x38, 0x58, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
]);
const PDF_STUB = new TextEncoder().encode("%PDF-1.4\n%%EOF\n");
const SVG_MARKUP = new TextEncoder().encode(
'<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>',
);
const XML_SVG = new TextEncoder().encode(
'<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg"/>',
);
describe("validateCreateFlowUploadBytes", () => {
it("accepts a real PNG as a community avatar", () => {
const result = validateCreateFlowUploadBytes("communityAvatar", PNG_1X1);
expect(result).toEqual({
ok: true,
kind: "png",
mimeType: "image/png",
});
});
it("accepts JPEG, GIF, and WebP headers", () => {
expect(validateCreateFlowUploadBytes("communityAvatar", JPEG_1X1_SOF).ok).toBe(
true,
);
expect(validateCreateFlowUploadBytes("communityAvatar", GIF_1X1).ok).toBe(
true,
);
expect(validateCreateFlowUploadBytes("communityAvatar", WEBP_1X1).ok).toBe(
true,
);
});
it("rejects an empty buffer", () => {
expect(validateCreateFlowUploadBytes("communityAvatar", new Uint8Array())).toEqual(
{ ok: false, reason: "empty" },
);
});
it("rejects a file larger than the purpose cap", () => {
const oversized = new Uint8Array(maxBytesForPurpose("communityAvatar") + 1);
oversized.set(PNG_1X1.subarray(0, 8), 0);
expect(validateCreateFlowUploadBytes("communityAvatar", oversized)).toEqual({
ok: false,
reason: "tooLarge",
});
expect(maxBytesForPurpose("customMethodAttachment")).toBeGreaterThan(
maxBytesForPurpose("communityAvatar"),
);
});
it("rejects text spoofed as PNG", () => {
expect(
validateCreateFlowUploadBytes(
"communityAvatar",
new TextEncoder().encode("hello world"),
),
).toEqual({ ok: false, reason: "invalidType" });
});
it("rejects SVG markup, including xml-prefixed SVG", () => {
expect(validateCreateFlowUploadBytes("communityAvatar", SVG_MARKUP)).toEqual(
{ ok: false, reason: "svg" },
);
expect(validateCreateFlowUploadBytes("customMethodAttachment", XML_SVG)).toEqual(
{ ok: false, reason: "svg" },
);
});
it("rejects a PNG signature that cannot be decoded", () => {
const truncated = PNG_1X1.subarray(0, 8);
expect(validateCreateFlowUploadBytes("communityAvatar", truncated)).toEqual({
ok: false,
reason: "undecodable",
});
});
it("rejects PDF for community avatars and allows it for attachments", () => {
expect(validateCreateFlowUploadBytes("communityAvatar", PDF_STUB)).toEqual({
ok: false,
reason: "invalidType",
});
expect(validateCreateFlowUploadBytes("customMethodAttachment", PDF_STUB)).toEqual(
{
ok: true,
kind: "pdf",
mimeType: "application/pdf",
},
);
});
});
describe("validateCreateFlowUploadFile", () => {
it("rejects empty files before reading bytes", async () => {
const file = new File([], "empty.png", { type: "image/png" });
await expect(
validateCreateFlowUploadFile(file, "communityAvatar"),
).resolves.toEqual({ ok: false, reason: "empty" });
});
it("rejects SVG by name even when the MIME is spoofed", async () => {
const file = new File([PNG_1X1], "logo.svg", { type: "image/png" });
await expect(
validateCreateFlowUploadFile(file, "communityAvatar"),
).resolves.toEqual({ ok: false, reason: "svg" });
});
it("rejects a 17MB file by size without treating it as a type error", async () => {
const file = new File([new Uint8Array([0x89, 0x50])], "huge.png", {
type: "image/png",
});
Object.defineProperty(file, "size", { value: 17 * 1024 * 1024 });
await expect(
validateCreateFlowUploadFile(file, "communityAvatar"),
).resolves.toEqual({ ok: false, reason: "tooLarge" });
});
});
describe("uploadCreateFlowFile", () => {
it("does not POST when client validation fails", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const file = new File(["not an image"], "photo.png", { type: "image/png" });
await expect(
uploadCreateFlowFile(file, "communityAvatar"),
).rejects.toBeInstanceOf(CreateFlowUploadValidationError);
expect(fetchMock).not.toHaveBeenCalled();
vi.unstubAllGlobals();
});
});