fix: Base64 uploads are not correctly verified for size limits (#10878)

This commit is contained in:
Tom Moor
2025-12-13 12:41:58 -05:00
committed by GitHub
parent 5c37f0a91d
commit a33731dd23
2 changed files with 203 additions and 0 deletions

View 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);
});
});
});
});

View File

@@ -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 = {