UNPKG

payload

Version:

Node, React, Headless CMS and Application Framework built on Next.js

315 lines (314 loc) • 8.82 kB
import { fileTypeFromBuffer } from 'file-type'; import { ValidationError } from '../errors/index.js'; import { validateMimeType } from '../utilities/validateMimeType.js'; import { validatePDF } from '../utilities/validatePDF.js'; import { detectSvgFromXml } from './detectSvgFromXml.js'; import { getFileTypeFallback } from './getFileTypeFallback.js'; import { validateSvg } from './validateSvg.js'; /** * Restricted file types and their extensions. */ export const RESTRICTED_FILE_EXT_AND_TYPES = [ { extensions: [ 'exe', 'dll' ], mimeType: 'application/x-msdownload' }, { extensions: [ 'exe', 'com', 'app', 'action' ], mimeType: 'application/x-executable' }, { extensions: [ 'bat', 'cmd' ], mimeType: 'application/x-msdos-program' }, { extensions: [ 'exe', 'com' ], mimeType: 'application/x-ms-dos-executable' }, { extensions: [ 'dmg' ], mimeType: 'application/x-apple-diskimage' }, { extensions: [ 'deb' ], mimeType: 'application/x-debian-package' }, { extensions: [ 'rpm' ], mimeType: 'application/x-redhat-package-manager' }, { extensions: [ 'exe', 'dll' ], mimeType: 'application/vnd.microsoft.portable-executable' }, { extensions: [ 'msi' ], mimeType: 'application/x-msi' }, { extensions: [ 'jar', 'ear', 'war' ], mimeType: 'application/java-archive' }, { extensions: [ 'desktop' ], mimeType: 'application/x-desktop' }, { extensions: [ 'cpl' ], mimeType: 'application/x-cpl' }, { extensions: [ 'lnk' ], mimeType: 'application/x-ms-shortcut' }, { extensions: [ 'pkg' ], mimeType: 'application/x-apple-installer' }, { extensions: [ 'htm', 'html', 'shtml', 'xhtml' ], mimeType: 'text/html' }, { extensions: [ 'php', 'phtml' ], mimeType: 'application/x-httpd-php' }, { extensions: [ 'js', 'jse' ], mimeType: 'text/javascript' }, { extensions: [ 'jsp' ], mimeType: 'application/x-jsp' }, { extensions: [ 'py' ], mimeType: 'text/x-python' }, { extensions: [ 'rb' ], mimeType: 'text/x-ruby' }, { extensions: [ 'pl' ], mimeType: 'text/x-perl' }, { extensions: [ 'ps1', 'psc1', 'psd1', 'psh', 'psm1' ], mimeType: 'application/x-powershell' }, { extensions: [ 'vbe', 'vbs' ], mimeType: 'application/x-vbscript' }, { extensions: [ 'ws', 'wsc', 'wsf', 'wsh' ], mimeType: 'application/x-ms-wsh' }, { extensions: [ 'scr' ], mimeType: 'application/x-msdownload' }, { extensions: [ 'asp', 'aspx' ], mimeType: 'application/x-asp' }, { extensions: [ 'hta' ], mimeType: 'application/x-hta' }, { extensions: [ 'reg' ], mimeType: 'application/x-registry' }, { extensions: [ 'url' ], mimeType: 'application/x-url' }, { extensions: [ 'workflow' ], mimeType: 'application/x-workflow' }, { extensions: [ 'command' ], mimeType: 'application/x-command' } ]; export const checkFileRestrictions = async ({ collection, file, req })=>{ const errors = []; const { upload: uploadConfig } = collection; const useTempFiles = req?.payload?.config?.upload?.useTempFiles ?? false; const configMimeTypes = uploadConfig && typeof uploadConfig === 'object' && 'mimeTypes' in uploadConfig && Array.isArray(uploadConfig.mimeTypes) ? uploadConfig.mimeTypes : []; const allowRestrictedFileTypes = uploadConfig && typeof uploadConfig === 'object' && 'allowRestrictedFileTypes' in uploadConfig ? uploadConfig.allowRestrictedFileTypes : false; const expectsDetectableType = (mimeType)=>{ const textBasedTypes = [ '/svg', 'image/svg+xml', 'image/x-xbitmap', 'image/x-xpixmap' ]; if (textBasedTypes.includes(mimeType)) { return false; } return mimeType.startsWith('image/') || mimeType.startsWith('video/') || mimeType.startsWith('audio/') || mimeType === 'application/pdf'; }; // Skip validation if `allowRestrictedFileTypes` is true if (allowRestrictedFileTypes) { return; } // Secondary mimetype check to assess file type from buffer if (configMimeTypes.length > 0) { let detected = await fileTypeFromBuffer(file.data); const typeFromExtension = file.name.split('.').pop() || ''; // Handle SVG files that are detected as XML due to <?xml declarations if (detected?.mime === 'application/xml' && configMimeTypes.some((type)=>type.includes('image/') && (type.includes('svg') || type === 'image/*'))) { const isSvg = detectSvgFromXml(file.data); if (isSvg) { detected = { ext: 'svg', mime: 'image/svg+xml' }; } } if (!detected && !useTempFiles) { const mimeTypeFromExtension = getFileTypeFallback(file.name).mime; const extIsValid = validateMimeType(mimeTypeFromExtension, configMimeTypes); if (!extIsValid) { errors.push(`File type ${mimeTypeFromExtension} (from extension ${typeFromExtension}) is not allowed.`); } else { // SVG security check (text-based files not detectable by buffer) if (typeFromExtension.toLowerCase() === 'svg') { const isSafeSvg = validateSvg(file.data); if (!isSafeSvg) { errors.push('SVG file contains potentially harmful content.'); } } // PDF validation if (mimeTypeFromExtension === 'application/pdf') { const isValidPDF = validatePDF(file.data); if (!isValidPDF) { errors.push('Invalid or corrupted PDF file.'); } } } if (expectsDetectableType(mimeTypeFromExtension)) { req.payload.logger.warn(`File buffer returned no detectable MIME type for ${file.name}. Falling back to extension-based validation.`); } } const passesMimeTypeCheck = detected?.mime && validateMimeType(detected.mime, configMimeTypes); if (passesMimeTypeCheck && detected?.mime === 'application/pdf') { const isValidPDF = validatePDF(file?.data); if (!isValidPDF) { errors.push('Invalid PDF file.'); } } if (detected && !passesMimeTypeCheck) { errors.push(`Invalid MIME type: ${detected.mime}.`); } } else { const isRestricted = RESTRICTED_FILE_EXT_AND_TYPES.some((type)=>{ const hasRestrictedExt = type.extensions.some((ext)=>file.name.toLowerCase().endsWith(ext)); const hasRestrictedMime = type.mimeType === file.mimetype; return hasRestrictedExt || hasRestrictedMime; }); if (isRestricted) { errors.push(`File type '${file.mimetype}' not allowed ${file.name}: Restricted file type detected -- set 'allowRestrictedFileTypes' to true to skip this check for this Collection.`); } } if (errors.length > 0) { req.payload.logger.error(errors.join(', ')); throw new ValidationError({ errors: [ { message: errors.join(', '), path: 'file' } ] }); } }; //# sourceMappingURL=checkFileRestrictions.js.map