mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
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:
@@ -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";
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user