mirror of
https://github.com/outline/outline.git
synced 2025-12-30 07:19:52 -06:00
fix: Base64 uploads are not correctly verified for size limits (#10878)
This commit is contained in:
188
server/storage/files/BaseStorage.test.ts
Normal file
188
server/storage/files/BaseStorage.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import BaseStorage from "./BaseStorage";
|
||||
import env from "@server/env";
|
||||
|
||||
/**
|
||||
* Mock implementation of BaseStorage for testing purposes.
|
||||
*/
|
||||
class MockStorage extends BaseStorage {
|
||||
public storedFiles: Array<{
|
||||
key: string;
|
||||
body: Buffer;
|
||||
contentType?: string;
|
||||
acl?: string;
|
||||
}> = [];
|
||||
|
||||
async getPresignedPost() {
|
||||
return {};
|
||||
}
|
||||
|
||||
async getFileStream() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getUploadUrl() {
|
||||
return "https://storage.example.com";
|
||||
}
|
||||
|
||||
getUrlForKey(key: string) {
|
||||
return `https://storage.example.com/${key}`;
|
||||
}
|
||||
|
||||
async getSignedUrl() {
|
||||
return "https://storage.example.com/signed";
|
||||
}
|
||||
|
||||
async store({
|
||||
body,
|
||||
contentType,
|
||||
key,
|
||||
acl,
|
||||
}: {
|
||||
body: Buffer;
|
||||
contentType?: string;
|
||||
key: string;
|
||||
acl?: string;
|
||||
}) {
|
||||
this.storedFiles.push({
|
||||
key,
|
||||
body: body as Buffer,
|
||||
contentType,
|
||||
acl,
|
||||
});
|
||||
return this.getUrlForKey(key);
|
||||
}
|
||||
|
||||
async getFileHandle() {
|
||||
return { path: "/tmp/test", cleanup: async () => {} };
|
||||
}
|
||||
|
||||
async getFileExists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async moveFile() {}
|
||||
|
||||
async deleteFile() {}
|
||||
}
|
||||
|
||||
describe("BaseStorage", () => {
|
||||
describe("storeFromUrl", () => {
|
||||
let storage: MockStorage;
|
||||
|
||||
beforeEach(() => {
|
||||
storage = new MockStorage();
|
||||
storage.storedFiles = [];
|
||||
env.FILE_STORAGE_UPLOAD_MAX_SIZE = 500;
|
||||
});
|
||||
|
||||
describe("base64 URL size validation", () => {
|
||||
it("should enforce maxUploadSize option for base64 URLs", async () => {
|
||||
const largeDataSize = 1000;
|
||||
const maxUploadSize = 500;
|
||||
|
||||
const largeBuffer = Buffer.alloc(largeDataSize, "a");
|
||||
const base64Data = largeBuffer.toString("base64");
|
||||
const base64Url = `data:image/png;base64,${base64Data}`;
|
||||
|
||||
const result = await storage.storeFromUrl(
|
||||
base64Url,
|
||||
"test-key",
|
||||
"public-read",
|
||||
undefined,
|
||||
{ maxUploadSize }
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(storage.storedFiles).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should enforce FILE_STORAGE_UPLOAD_MAX_SIZE for base64 URLs", async () => {
|
||||
const largeDataSize = 1000; // Exceeds our test limit of 500
|
||||
|
||||
const largeBuffer = Buffer.alloc(largeDataSize, "a");
|
||||
const base64Data = largeBuffer.toString("base64");
|
||||
const base64Url = `data:image/png;base64,${base64Data}`;
|
||||
|
||||
const result = await storage.storeFromUrl(
|
||||
base64Url,
|
||||
"test-key",
|
||||
"public-read"
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(storage.storedFiles).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should allow base64 URLs within size limits", async () => {
|
||||
const dataSize = 100;
|
||||
const maxUploadSize = 500;
|
||||
|
||||
const buffer = Buffer.alloc(dataSize, "a");
|
||||
const base64Data = buffer.toString("base64");
|
||||
const base64Url = `data:image/png;base64,${base64Data}`;
|
||||
|
||||
const result = await storage.storeFromUrl(
|
||||
base64Url,
|
||||
"test-key",
|
||||
"public-read",
|
||||
undefined,
|
||||
{ maxUploadSize }
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.contentLength).toBe(dataSize);
|
||||
expect(result?.contentType).toBe("image/png");
|
||||
expect(storage.storedFiles).toHaveLength(1);
|
||||
expect(storage.storedFiles[0].body.byteLength).toBe(dataSize);
|
||||
});
|
||||
|
||||
it("should use the minimum of maxUploadSize and FILE_STORAGE_UPLOAD_MAX_SIZE", async () => {
|
||||
// Create data that's larger than maxUploadSize but smaller than FILE_STORAGE_UPLOAD_MAX_SIZE
|
||||
const maxUploadSize = 200;
|
||||
const dataSize = 300;
|
||||
|
||||
const buffer = Buffer.alloc(dataSize, "a");
|
||||
const base64Data = buffer.toString("base64");
|
||||
const base64Url = `data:image/png;base64,${base64Data}`;
|
||||
|
||||
const result = await storage.storeFromUrl(
|
||||
base64Url,
|
||||
"test-key",
|
||||
"public-read",
|
||||
undefined,
|
||||
{ maxUploadSize }
|
||||
);
|
||||
|
||||
// Should be rejected because it exceeds maxUploadSize even though it's under FILE_STORAGE_UPLOAD_MAX_SIZE
|
||||
expect(result).toBeUndefined();
|
||||
expect(storage.storedFiles).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should return undefined for empty base64 data", async () => {
|
||||
const base64Url = "data:image/png;base64,";
|
||||
|
||||
const result = await storage.storeFromUrl(
|
||||
base64Url,
|
||||
"test-key",
|
||||
"public-read"
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(storage.storedFiles).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("internal URL handling", () => {
|
||||
it("should return undefined for URLs already on the storage provider", async () => {
|
||||
const result = await storage.storeFromUrl(
|
||||
"https://storage.example.com/existing-file.png",
|
||||
"test-key",
|
||||
"public-read"
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(storage.storedFiles).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -164,6 +164,21 @@ export default abstract class BaseStorage {
|
||||
if (match) {
|
||||
contentType = match[1];
|
||||
buffer = Buffer.from(match[2], "base64");
|
||||
|
||||
// Validate size for base64 URLs, same as for remote URLs
|
||||
const maxSize = Math.min(
|
||||
options?.maxUploadSize ?? Infinity,
|
||||
env.FILE_STORAGE_UPLOAD_MAX_SIZE
|
||||
);
|
||||
|
||||
if (buffer.byteLength > maxSize) {
|
||||
Logger.warn("Base64 URL exceeds size limit", {
|
||||
size: buffer.byteLength,
|
||||
maxSize,
|
||||
key,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const headers = {
|
||||
|
||||
Reference in New Issue
Block a user