[VPAT-16] improvement: add file validation to prevent malicious uploads #8493
Add client-side checks for double extensions, dangerous file types, dot files, and path traversal patterns. Addresses security audit recommendations for file upload validation.
This commit is contained in:
parent
49fc6aa0a0
commit
e10deb10f2
2 changed files with 70 additions and 10 deletions
|
|
@ -18,3 +18,22 @@ export const ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE = {
|
||||||
"image/png": [],
|
"image/png": [],
|
||||||
"image/webp": [],
|
"image/webp": [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dangerous file extensions that should be blocked
|
||||||
|
*/
|
||||||
|
export const DANGEROUS_EXTENSIONS = [
|
||||||
|
"exe",
|
||||||
|
"bat",
|
||||||
|
"cmd",
|
||||||
|
"sh",
|
||||||
|
"php",
|
||||||
|
"asp",
|
||||||
|
"aspx",
|
||||||
|
"jsp",
|
||||||
|
"cgi",
|
||||||
|
"dll",
|
||||||
|
"vbs",
|
||||||
|
"jar",
|
||||||
|
"ps1",
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,46 @@
|
||||||
import { fileTypeFromBuffer } from "file-type";
|
import { fileTypeFromBuffer } from "file-type";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { TFileMetaDataLite, TFileSignedURLResponse } from "@plane/types";
|
import type { TFileMetaDataLite, TFileSignedURLResponse } from "@plane/types";
|
||||||
|
import { DANGEROUS_EXTENSIONS } from "@plane/constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Filename validation - checks for double extensions and dangerous patterns
|
||||||
|
* @param {string} filename
|
||||||
|
* @returns {string | null} Error message if invalid, null if valid
|
||||||
|
*/
|
||||||
|
const validateFilename = (filename: string): string | null => {
|
||||||
|
if (!filename || filename.trim().length === 0) {
|
||||||
|
return "Filename cannot be empty";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dot files (e.g., .htaccess, .env)
|
||||||
|
if (filename.startsWith(".")) {
|
||||||
|
return "Hidden files (starting with dot) are not allowed";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path separators
|
||||||
|
if (filename.includes("/") || filename.includes("\\")) {
|
||||||
|
return "Filename cannot contain path separators";
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = filename.split(".");
|
||||||
|
|
||||||
|
// Check for double extensions with dangerous patterns
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
const secondLastExt = parts[parts.length - 2]?.toLowerCase() || "";
|
||||||
|
if (DANGEROUS_EXTENSIONS.includes(secondLastExt)) {
|
||||||
|
return "File has suspicious double extension";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the actual extension is dangerous
|
||||||
|
const extension = parts[parts.length - 1]?.toLowerCase() || "";
|
||||||
|
if (DANGEROUS_EXTENSIONS.includes(extension)) {
|
||||||
|
return `File extension '${extension}' is not allowed`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description from the provided signed URL response, generate a payload to be used to upload the file
|
* @description from the provided signed URL response, generate a payload to be used to upload the file
|
||||||
|
|
@ -42,28 +82,29 @@ const detectMimeTypeFromSignature = async (file: File): Promise<string> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Determine the MIME type of a file using multiple detection methods
|
* @description Validate and detect the MIME type of a file using signature detection
|
||||||
|
* Also performs basic security checks on filename
|
||||||
* @param {File} file
|
* @param {File} file
|
||||||
* @returns {Promise<string>} detected MIME type
|
* @returns {Promise<string>} validated and detected MIME type
|
||||||
*/
|
*/
|
||||||
const detectFileType = async (file: File): Promise<string> => {
|
const validateAndDetectFileType = async (file: File): Promise<string> => {
|
||||||
// check if the file has a MIME type
|
// Basic filename validation
|
||||||
if (file.type && file.type.trim() !== "") {
|
const filenameError = validateFilename(file.name);
|
||||||
return file.type;
|
if (filenameError) {
|
||||||
|
console.warn(`File validation warning: ${filenameError}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// detect from file signature using file-type library
|
|
||||||
try {
|
try {
|
||||||
const signatureType = await detectMimeTypeFromSignature(file);
|
const signatureType = await detectMimeTypeFromSignature(file);
|
||||||
if (signatureType) {
|
if (signatureType) {
|
||||||
return signatureType;
|
return signatureType;
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
console.error("Error detecting file type from signature:", _error);
|
console.warn("Error detecting file type from signature:", _error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback for unknown files
|
// fallback for unknown files
|
||||||
return "application/octet-stream";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -72,7 +113,7 @@ const detectFileType = async (file: File): Promise<string> => {
|
||||||
* @returns {Promise<TFileMetaDataLite>} payload with file info
|
* @returns {Promise<TFileMetaDataLite>} payload with file info
|
||||||
*/
|
*/
|
||||||
export const getFileMetaDataForUpload = async (file: File): Promise<TFileMetaDataLite> => {
|
export const getFileMetaDataForUpload = async (file: File): Promise<TFileMetaDataLite> => {
|
||||||
const fileType = await detectFileType(file);
|
const fileType = await validateAndDetectFileType(file);
|
||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue