feat: implement file extension blacklist for upload security (#27540)
This commit is contained in:
@@ -11,6 +11,7 @@ import type { FileEntity } from './types'
|
||||
import { useFileStore } from './store'
|
||||
import {
|
||||
fileUpload,
|
||||
getFileUploadErrorMessage,
|
||||
getSupportFileType,
|
||||
isAllowedFileExtension,
|
||||
} from './utils'
|
||||
@@ -172,8 +173,9 @@ export const useFile = (fileConfig: FileUpload) => {
|
||||
onSuccessCallback: (res) => {
|
||||
handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
|
||||
},
|
||||
onErrorCallback: () => {
|
||||
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
|
||||
onErrorCallback: (error?: any) => {
|
||||
const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t)
|
||||
notify({ type: 'error', message: errorMessage })
|
||||
handleUpdateFile({ ...uploadingFile, progress: -1 })
|
||||
},
|
||||
}, !!params.token)
|
||||
@@ -279,8 +281,9 @@ export const useFile = (fileConfig: FileUpload) => {
|
||||
onSuccessCallback: (res) => {
|
||||
handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
|
||||
},
|
||||
onErrorCallback: () => {
|
||||
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
|
||||
onErrorCallback: (error?: any) => {
|
||||
const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t)
|
||||
notify({ type: 'error', message: errorMessage })
|
||||
handleUpdateFile({ ...uploadingFile, progress: -1 })
|
||||
},
|
||||
}, !!params.token)
|
||||
|
||||
@@ -7,11 +7,30 @@ import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import type { FileResponse } from '@/types/workflow'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
||||
/**
|
||||
* Get appropriate error message for file upload errors
|
||||
* @param error - The error object from upload failure
|
||||
* @param defaultMessage - Default error message to use if no specific error is matched
|
||||
* @param t - Translation function
|
||||
* @returns Localized error message
|
||||
*/
|
||||
export const getFileUploadErrorMessage = (error: any, defaultMessage: string, t: (key: string) => string): string => {
|
||||
const errorCode = error?.response?.code
|
||||
|
||||
if (errorCode === 'forbidden')
|
||||
return error?.response?.message
|
||||
|
||||
if (errorCode === 'file_extension_blocked')
|
||||
return t('common.fileUploader.fileExtensionBlocked')
|
||||
|
||||
return defaultMessage
|
||||
}
|
||||
|
||||
type FileUploadParams = {
|
||||
file: File
|
||||
onProgressCallback: (progress: number) => void
|
||||
onSuccessCallback: (res: { id: string }) => void
|
||||
onErrorCallback: () => void
|
||||
onErrorCallback: (error?: any) => void
|
||||
}
|
||||
type FileUpload = (v: FileUploadParams, isPublic?: boolean, url?: string) => void
|
||||
export const fileUpload: FileUpload = ({
|
||||
@@ -37,8 +56,8 @@ export const fileUpload: FileUpload = ({
|
||||
.then((res: { id: string }) => {
|
||||
onSuccessCallback(res)
|
||||
})
|
||||
.catch(() => {
|
||||
onErrorCallback()
|
||||
.catch((error) => {
|
||||
onErrorCallback(error)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import type { ClipboardEvent } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { imageUpload } from './utils'
|
||||
import { getImageUploadErrorMessage, imageUpload } from './utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { ALLOW_FILE_EXTENSIONS, TransferMethod } from '@/types/app'
|
||||
import type { ImageFile, VisionSettings } from '@/types/app'
|
||||
@@ -81,8 +81,9 @@ export const useImageFiles = () => {
|
||||
filesRef.current = newFiles
|
||||
setFiles(newFiles)
|
||||
},
|
||||
onErrorCallback: () => {
|
||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
|
||||
onErrorCallback: (error?: any) => {
|
||||
const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t)
|
||||
notify({ type: 'error', message: errorMessage })
|
||||
const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: -1 }, ...files.slice(index + 1)]
|
||||
filesRef.current = newFiles
|
||||
setFiles(newFiles)
|
||||
@@ -158,8 +159,9 @@ export const useLocalFileUploader = ({ limit, disabled = false, onUpload }: useL
|
||||
onSuccessCallback: (res) => {
|
||||
onUpload({ ...imageFile, fileId: res.id, progress: 100 })
|
||||
},
|
||||
onErrorCallback: () => {
|
||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
|
||||
onErrorCallback: (error?: any) => {
|
||||
const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t)
|
||||
notify({ type: 'error', message: errorMessage })
|
||||
onUpload({ ...imageFile, progress: -1 })
|
||||
},
|
||||
}, !!params.token)
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import { upload } from '@/service/base'
|
||||
|
||||
/**
|
||||
* Get appropriate error message for image upload errors
|
||||
* @param error - The error object from upload failure
|
||||
* @param defaultMessage - Default error message to use if no specific error is matched
|
||||
* @param t - Translation function
|
||||
* @returns Localized error message
|
||||
*/
|
||||
export const getImageUploadErrorMessage = (error: any, defaultMessage: string, t: (key: string) => string): string => {
|
||||
const errorCode = error?.response?.code
|
||||
|
||||
if (errorCode === 'forbidden')
|
||||
return error?.response?.message
|
||||
|
||||
if (errorCode === 'file_extension_blocked')
|
||||
return t('common.fileUploader.fileExtensionBlocked')
|
||||
|
||||
return defaultMessage
|
||||
}
|
||||
|
||||
type ImageUploadParams = {
|
||||
file: File
|
||||
onProgressCallback: (progress: number) => void
|
||||
onSuccessCallback: (res: { id: string }) => void
|
||||
onErrorCallback: () => void
|
||||
onErrorCallback: (error?: any) => void
|
||||
}
|
||||
type ImageUpload = (v: ImageUploadParams, isPublic?: boolean, url?: string) => void
|
||||
export const imageUpload: ImageUpload = ({
|
||||
@@ -30,7 +49,7 @@ export const imageUpload: ImageUpload = ({
|
||||
.then((res: { id: string }) => {
|
||||
onSuccessCallback(res)
|
||||
})
|
||||
.catch(() => {
|
||||
onErrorCallback()
|
||||
.catch((error) => {
|
||||
onErrorCallback(error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { imageUpload } from '@/app/components/base/image-uploader/utils'
|
||||
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { BubbleTextMod } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import {
|
||||
@@ -67,8 +67,9 @@ const CustomWebAppBrand = () => {
|
||||
setUploadProgress(100)
|
||||
setFileId(res.id)
|
||||
},
|
||||
onErrorCallback: () => {
|
||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
|
||||
onErrorCallback: (error?: any) => {
|
||||
const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t)
|
||||
notify({ type: 'error', message: errorMessage })
|
||||
setUploadProgress(-1)
|
||||
},
|
||||
}, false, '/workspaces/custom-config/webapp-logo/upload')
|
||||
|
||||
@@ -18,6 +18,7 @@ import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { Theme } from '@/types/app'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
|
||||
|
||||
type IFileUploaderProps = {
|
||||
fileList: FileItem[]
|
||||
@@ -134,7 +135,8 @@ const FileUploader = ({
|
||||
return Promise.resolve({ ...completeFile })
|
||||
})
|
||||
.catch((e) => {
|
||||
notify({ type: 'error', message: e?.response?.code === 'forbidden' ? e?.response?.message : t('datasetCreation.stepOne.uploader.failed') })
|
||||
const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t)
|
||||
notify({ type: 'error', message: errorMessage })
|
||||
onFileUpdate(fileItem, -2, fileListRef.current)
|
||||
return Promise.resolve({ ...fileItem })
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import cn from '@/utils/classnames'
|
||||
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { upload } from '@/service/base'
|
||||
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
|
||||
import I18n from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
@@ -154,7 +155,8 @@ const LocalFile = ({
|
||||
return Promise.resolve({ ...completeFile })
|
||||
})
|
||||
.catch((e) => {
|
||||
notify({ type: 'error', message: e?.response?.code === 'forbidden' ? e?.response?.message : t('datasetCreation.stepOne.uploader.failed') })
|
||||
const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t)
|
||||
notify({ type: 'error', message: errorMessage })
|
||||
updateFile(fileItem, -2, fileListRef.current)
|
||||
return Promise.resolve({ ...fileItem })
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ToastContext } from '@/app/components/base/toast'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { FileItem } from '@/models/datasets'
|
||||
import { upload } from '@/service/base'
|
||||
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
|
||||
import useSWR from 'swr'
|
||||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
import SimplePieChart from '@/app/components/base/simple-pie-chart'
|
||||
@@ -74,7 +75,8 @@ const CSVUploader: FC<Props> = ({
|
||||
return Promise.resolve({ ...completeFile })
|
||||
})
|
||||
.catch((e) => {
|
||||
notify({ type: 'error', message: e?.response?.code === 'forbidden' ? e?.response?.message : t('datasetCreation.stepOne.uploader.failed') })
|
||||
const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t)
|
||||
notify({ type: 'error', message: errorMessage })
|
||||
const errorFile = {
|
||||
...fileItem,
|
||||
progress: -2,
|
||||
|
||||
Reference in New Issue
Block a user