fix: Potential unsafe content-type check (#8673)

* fix: Potential bypass of content-type check

* Include extra available chars
This commit is contained in:
Tom Moor
2025-03-12 08:39:41 -04:00
committed by GitHub
parent 70268a73df
commit 516d14fe27
4 changed files with 61 additions and 21 deletions
+9 -13
View File
@@ -1,6 +1,7 @@
import { Blob } from "buffer";
import { Readable } from "stream";
import { PresignedPost } from "@aws-sdk/s3-presigned-post";
import FileHelper from "@shared/editor/lib/FileHelper";
import { isBase64Url, isInternalUrl } from "@shared/utils/urls";
import env from "@server/env";
import Logger from "@server/logging/Logger";
@@ -239,14 +240,14 @@ export default abstract class BaseStorage {
* @returns The content disposition
*/
public getContentDisposition(contentType?: string) {
if (contentType && this.safeInlineContentTypes.includes(contentType)) {
return "inline";
if (!contentType) {
return "attachment";
}
if (
contentType &&
this.safeInlineContentPrefixes.some((prefix) =>
contentType.startsWith(prefix)
)
FileHelper.isAudio(contentType) ||
FileHelper.isVideo(contentType) ||
this.safeInlineContentTypes.includes(contentType)
) {
return "inline";
}
@@ -255,8 +256,8 @@ export default abstract class BaseStorage {
}
/**
* A list of content types considered safe to display inline in the browser. Note that
* SVGs are purposefully not included here as they can contain JavaScript.
* A list of content types considered safe to display inline in the browser.
* Note that SVGs are purposefully not included here as they can contain JS.
*/
protected safeInlineContentTypes = [
"application/pdf",
@@ -265,9 +266,4 @@ export default abstract class BaseStorage {
"image/gif",
"image/webp",
];
/**
* A list of content type prefixes considered safe to display inline in the browser.
*/
protected safeInlineContentPrefixes = ["video/", "audio/"];
}
+2 -2
View File
@@ -58,11 +58,11 @@ const insertFiles = async function (
const filesToUpload = await Promise.all(
files.map(async (file) => {
const isImage =
FileHelper.isImage(file) &&
FileHelper.isImage(file.type) &&
!options.isAttachment &&
!!schema.nodes.image;
const isVideo =
FileHelper.isVideo(file) &&
FileHelper.isVideo(file.type) &&
!options.isAttachment &&
!!schema.nodes.video;
const getDimensions = isImage
+34
View File
@@ -0,0 +1,34 @@
import FileHelper from "./FileHelper";
describe("FileHelper", () => {
it("isImage", () => {
expect(FileHelper.isImage("image/png")).toBe(true);
expect(FileHelper.isImage("image/jpeg")).toBe(true);
expect(FileHelper.isImage("image/webp")).toBe(true);
expect(FileHelper.isImage("image/gif")).toBe(true);
expect(FileHelper.isImage("image/bmp")).toBe(true);
expect(FileHelper.isImage("image/avif")).toBe(true);
expect(FileHelper.isImage("image/heif-sequence")).toBe(true);
expect(FileHelper.isImage("image/svg+xml")).toBe(true);
expect(FileHelper.isImage("text/plain")).toBe(false);
expect(FileHelper.isImage("application/json")).toBe(false);
});
it("isVideo", () => {
expect(FileHelper.isVideo("video/mp4")).toBe(true);
expect(FileHelper.isVideo("video/webm")).toBe(true);
expect(FileHelper.isVideo("video/x-msvideo")).toBe(true);
expect(FileHelper.isVideo("video/vnd.dlna.mpeg-tts")).toBe(true);
expect(FileHelper.isVideo("text/plain")).toBe(false);
expect(FileHelper.isVideo("application/json")).toBe(false);
});
it("isAudio", () => {
expect(FileHelper.isAudio("audio/mpeg")).toBe(true);
expect(FileHelper.isAudio("audio/wav")).toBe(true);
expect(FileHelper.isAudio("audio/vnd.dolby.heaac.1")).toBe(true);
expect(FileHelper.isAudio("audio/vnd.lucent.voice")).toBe(true);
expect(FileHelper.isAudio("text/plain")).toBe(false);
expect(FileHelper.isAudio("application/json")).toBe(false);
});
});
+16 -6
View File
@@ -4,21 +4,31 @@ export default class FileHelper {
/**
* Checks if a file is an image.
*
* @param file The file to check
* @param contentType The content type of the file
* @returns True if the file is an image
*/
static isImage(file: File) {
return file.type.startsWith("image/");
static isImage(contentType: string) {
return /^image\/[!#$%&'*+.^\w`|~-]+$/i.test(contentType);
}
/**
* Checks if a file is a video.
*
* @param file The file to check
* @param contentType The content type of the file
* @returns True if the file is an video
*/
static isVideo(file: File) {
return file.type.startsWith("video/");
static isVideo(contentType: string) {
return /^video\/[!#$%&'*+.^\w`|~-]+$/i.test(contentType);
}
/**
* Checks if a file is an audio file.
*
* @param contentType The content type of the file
* @returns True if the file is an audio file
*/
static isAudio(contentType: string) {
return /^audio\/[!#$%&'*+.^\w`|~-]+$/i.test(contentType);
}
/**