fix: Correctly validate uploaded file size using "local" storage option (#12095)

* fix: Correctly validate uploaded file size using local storage option

* fix: Normalize attachment size from BIGINT before comparison

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-04-17 23:25:46 -04:00
committed by GitHub
parent 347bdb10d4
commit 505082b196
5 changed files with 109 additions and 2 deletions
+64
View File
@@ -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";
+15
View File
@@ -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,
};
@@ -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", () => {
+1 -1
View File
@@ -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"),
+1 -1
View File
@@ -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"),