diff --git a/plugins/storage/server/api/files.test.ts b/plugins/storage/server/api/files.test.ts index dbac6720e0..3a489486ec 100644 --- a/plugins/storage/server/api/files.test.ts +++ b/plugins/storage/server/api/files.test.ts @@ -98,6 +98,70 @@ describe("#files.create", () => { expect(res.status).toEqual(400); }); + it("should fail with status 400 if uploaded file exceeds declared size", async () => { + const user = await buildUser(); + const fileName = "images.docx"; + const attachment = await buildAttachment( + { + teamId: user.teamId, + userId: user.id, + size: 100, + contentType: + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }, + fileName + ); + + const content = await readFile( + path.resolve(__dirname, "..", "test", "fixtures", fileName) + ); + const form = new FormData(); + form.append("key", attachment.key); + form.append("file", content, fileName); + form.append("token", user.getJwtToken()); + + const res = await server.post(`/api/files.create`, { + headers: form.getHeaders(), + body: form, + }); + expect(res.status).toEqual(400); + expect( + existsSync(path.join(env.FILE_STORAGE_LOCAL_ROOT_DIR, attachment.key)) + ).toBe(false); + }); + + it("should update attachment size to actual uploaded bytes", async () => { + const user = await buildUser(); + const fileName = "images.docx"; + const attachment = await buildAttachment( + { + teamId: user.teamId, + userId: user.id, + size: 1_000_000, + contentType: + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }, + fileName + ); + + const content = await readFile( + path.resolve(__dirname, "..", "test", "fixtures", fileName) + ); + const form = new FormData(); + form.append("key", attachment.key); + form.append("file", content, fileName); + form.append("token", user.getJwtToken()); + + const res = await server.post(`/api/files.create`, { + headers: form.getHeaders(), + body: form, + }); + expect(res.status).toEqual(200); + + await attachment.reload(); + expect(Number(attachment.size)).toEqual(content.byteLength); + }); + it("should succeed with status 200 ok and create a file", async () => { const user = await buildUser(); const fileName = "images.docx"; diff --git a/plugins/storage/server/api/files.ts b/plugins/storage/server/api/files.ts index 89a470e7d0..24b899f8df 100644 --- a/plugins/storage/server/api/files.ts +++ b/plugins/storage/server/api/files.ts @@ -2,6 +2,7 @@ import JWT from "jsonwebtoken"; import Router from "koa-router"; import mime from "mime-types"; import contentDisposition from "content-disposition"; +import { bytesToHumanReadable } from "@shared/utils/files"; import env from "@server/env"; import { AuthenticationError, @@ -52,6 +53,16 @@ router.post( throw AuthorizationError("Invalid key"); } + const declaredSize = Number(attachment.size); + + if (file.size > declaredSize) { + throw ValidationError( + `The uploaded file exceeds the declared size of ${bytesToHumanReadable( + declaredSize + )}` + ); + } + try { await attachment.writeFile(file); } catch (err) { @@ -63,6 +74,10 @@ router.post( throw err; } + if (declaredSize !== file.size) { + await attachment.update({ size: file.size }, { silent: true }); + } + ctx.body = { success: true, }; diff --git a/server/routes/api/attachments/attachments.test.ts b/server/routes/api/attachments/attachments.test.ts index 46eef4b489..27d052d172 100644 --- a/server/routes/api/attachments/attachments.test.ts +++ b/server/routes/api/attachments/attachments.test.ts @@ -227,6 +227,34 @@ describe("#attachments.create", () => { }); expect(res.status).toEqual(400); }); + + it("should reject negative size", async () => { + const user = await buildUser(); + const res = await server.post("/api/attachments.create", { + body: { + name: "test.png", + contentType: "image/png", + size: -1, + preset: AttachmentPreset.Emoji, + token: user.getJwtToken(), + }, + }); + expect(res.status).toEqual(400); + }); + + it("should reject non-integer size", async () => { + const user = await buildUser(); + const res = await server.post("/api/attachments.create", { + body: { + name: "test.png", + contentType: "image/png", + size: 1.5, + preset: AttachmentPreset.Emoji, + token: user.getJwtToken(), + }, + }); + expect(res.status).toEqual(400); + }); }); describe("viewer", () => { diff --git a/server/routes/api/attachments/schema.ts b/server/routes/api/attachments/schema.ts index 35d81abe34..5fd6679775 100644 --- a/server/routes/api/attachments/schema.ts +++ b/server/routes/api/attachments/schema.ts @@ -26,7 +26,7 @@ export const AttachmentsCreateSchema = BaseSchema.extend({ documentId: z.uuid().optional(), /** File size of the Attachment */ - size: z.number(), + size: z.number().int().nonnegative(), /** Content-Type of the Attachment */ contentType: z.string().optional().prefault("application/octet-stream"), diff --git a/server/test/factories.ts b/server/test/factories.ts index b141406a15..0315dd111a 100644 --- a/server/test/factories.ts +++ b/server/test/factories.ts @@ -615,7 +615,7 @@ export async function buildAttachment( id, key: AttachmentHelper.getKey({ id, name, userId: overrides.userId }), contentType: "image/png", - size: 100, + size: 1_000_000, acl, name, createdAt: new Date("2018-01-02T00:00:00.000Z"),