-
-
Notifications
You must be signed in to change notification settings - Fork 24.2k
Expand file tree
/
Copy pathcreateAttachment.ts
More file actions
241 lines (210 loc) · 9.75 KB
/
createAttachment.ts
File metadata and controls
241 lines (210 loc) · 9.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import { Request } from 'express'
import * as path from 'path'
import {
addArrayFilesToStorage,
getFileFromUpload,
IDocument,
mapExtToInputField,
mapMimeTypeToInputField,
removeSpecificFileFromUpload,
removeSpecificFileFromStorage,
isValidUUID,
isPathTraversal
} from 'flowise-components'
import { getRunningExpressApp } from './getRunningExpressApp'
import { validateFileMimeTypeAndExtensionMatch } from './fileValidation'
import logger from './logger'
import { getErrorMessage } from '../errors/utils'
import { checkStorage, updateStorageUsage } from './quotaUsage'
import { ChatFlow } from '../database/entities/ChatFlow'
import { Workspace } from '../enterprise/database/entities/workspace.entity'
import { Organization } from '../enterprise/database/entities/organization.entity'
import { InternalFlowiseError } from '../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
/**
* Create attachment
* @param {Request} req
*/
export const createFileAttachment = async (req: Request) => {
const appServer = getRunningExpressApp()
const chatflowid = req.params.chatflowId
const chatId = req.params.chatId
if (!chatflowid || !isValidUUID(chatflowid)) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'Invalid chatflowId format - must be a valid UUID')
}
if (isPathTraversal(chatflowid) || (chatId && isPathTraversal(chatId))) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'Invalid path characters detected')
}
// Validate chatflow exists and check API key
const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
id: chatflowid
})
if (!chatflow) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Chatflow ${chatflowid} not found`)
}
let orgId = req.user?.activeOrganizationId || ''
let workspaceId = req.user?.activeWorkspaceId || ''
let subscriptionId = req.user?.activeOrganizationSubscriptionId || ''
// This is one of the WHITELIST_URLS, API can be public and there might be no req.user
if (!orgId || !workspaceId) {
const chatflowWorkspaceId = chatflow.workspaceId
const workspace = await appServer.AppDataSource.getRepository(Workspace).findOneBy({
id: chatflowWorkspaceId
})
if (!workspace) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Workspace ${chatflowWorkspaceId} not found`)
}
workspaceId = workspace.id
const org = await appServer.AppDataSource.getRepository(Organization).findOneBy({
id: workspace.organizationId
})
if (!org) {
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Organization ${workspace.organizationId} not found`)
}
orgId = org.id
subscriptionId = org.subscriptionId as string
}
// Parse chatbot configuration to get file upload settings
let pdfConfig = {
usage: 'perPage',
legacyBuild: false
}
let allowedFileTypes: string[] = []
let fileUploadEnabled = false
if (chatflow.chatbotConfig) {
try {
const chatbotConfig = JSON.parse(chatflow.chatbotConfig)
if (chatbotConfig?.fullFileUpload) {
fileUploadEnabled = chatbotConfig.fullFileUpload.status
// Get allowed file types from configuration
if (chatbotConfig.fullFileUpload.allowedUploadFileTypes) {
allowedFileTypes = chatbotConfig.fullFileUpload.allowedUploadFileTypes.split(',')
}
// PDF specific configuration
if (chatbotConfig.fullFileUpload.pdfFile) {
if (chatbotConfig.fullFileUpload.pdfFile.usage) {
pdfConfig.usage = chatbotConfig.fullFileUpload.pdfFile.usage
}
if (chatbotConfig.fullFileUpload.pdfFile.legacyBuild !== undefined) {
pdfConfig.legacyBuild = chatbotConfig.fullFileUpload.pdfFile.legacyBuild
}
}
}
} catch (e) {
// Use default config if parsing fails
}
}
// Check if file upload is enabled
if (!fileUploadEnabled) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'File upload is not enabled for this chatflow')
}
// Find FileLoader node
const fileLoaderComponent = appServer.nodesPool.componentNodes['fileLoader']
const fileLoaderNodeInstanceFilePath = fileLoaderComponent.filePath as string
const fileLoaderNodeModule = await import(fileLoaderNodeInstanceFilePath)
const fileLoaderNodeInstance = new fileLoaderNodeModule.nodeClass()
const options = {
retrieveAttachmentChatId: true,
orgId,
workspaceId,
chatflowid,
chatId
}
const files = (req.files as Express.Multer.File[]) || []
const fileAttachments = []
if (files.length) {
const isBase64 = req.body.base64
for (const file of files) {
if (!allowedFileTypes.length) {
throw new InternalFlowiseError(
StatusCodes.BAD_REQUEST,
`File type '${file.mimetype}' is not allowed. Allowed types: ${allowedFileTypes.join(', ')}`
)
}
// Validate file type against allowed types
if (allowedFileTypes.length > 0 && !allowedFileTypes.includes(file.mimetype)) {
throw new InternalFlowiseError(
StatusCodes.BAD_REQUEST,
`File type '${file.mimetype}' is not allowed. Allowed types: ${allowedFileTypes.join(', ')}`
)
}
// Security fix: Verify file extension matches the declared MIME type
// This prevents MIME type spoofing attacks (e.g., uploading .js file with text/plain MIME type)
// This addresses the vulnerability (CVE-2025-61687)
validateFileMimeTypeAndExtensionMatch(file.originalname, file.mimetype)
await checkStorage(orgId, subscriptionId, appServer.usageCacheManager)
const fileBuffer = await getFileFromUpload(file.path ?? file.key)
const fileNames: string[] = []
// Address file name with special characters: https://github.com/expressjs/multer/issues/1104
file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8')
const { path: storagePath, totalSize } = await addArrayFilesToStorage(
file.mimetype,
fileBuffer,
file.originalname,
fileNames,
orgId,
chatflowid,
chatId
)
await updateStorageUsage(orgId, workspaceId, totalSize, appServer.usageCacheManager)
const fileInputFieldFromMimeType = mapMimeTypeToInputField(file.mimetype)
const fileExtension = path.extname(file.originalname)
const fileInputFieldFromExt = mapExtToInputField(fileExtension)
let fileInputField = 'txtFile'
if (fileInputFieldFromExt !== 'txtFile') {
fileInputField = fileInputFieldFromExt
} else if (fileInputFieldFromMimeType !== 'txtFile') {
fileInputField = fileInputFieldFromMimeType
}
await removeSpecificFileFromUpload(file.path ?? file.key)
// Track sanitized filename for cleanup if processing fails
const sanitizedFilename = fileNames.length > 0 ? fileNames[0] : undefined
try {
const nodeData = {
inputs: {
[fileInputField]: storagePath
},
outputs: { output: 'document' }
}
// Apply PDF specific configuration if this is a PDF file
if (fileInputField === 'pdfFile') {
nodeData.inputs.usage = pdfConfig.usage
nodeData.inputs.legacyBuild = pdfConfig.legacyBuild as unknown as string
}
let content = ''
if (isBase64) {
content = fileBuffer.toString('base64')
} else {
const documents: IDocument[] = await fileLoaderNodeInstance.init(nodeData, '', options)
content = documents.map((doc) => doc.pageContent).join('\n')
}
fileAttachments.push({
name: file.originalname,
mimeType: file.mimetype,
size: file.size,
content
})
} catch (error) {
// Security: Clean up storage if processing failed, which includes invalid file type or content detacted from loader
if (sanitizedFilename) {
logger.info(`Clean up storage for ${file.originalname} (${sanitizedFilename}). Reason: ${getErrorMessage(error)}`)
try {
const { totalSize: newTotalSize } = await removeSpecificFileFromStorage(
orgId,
chatflowid,
chatId,
sanitizedFilename
)
await updateStorageUsage(orgId, workspaceId, newTotalSize, appServer.usageCacheManager)
} catch (cleanupError) {
logger.error(
`Failed to cleanup storage for ${file.originalname} (${sanitizedFilename}) - ${getErrorMessage(cleanupError)}`
)
}
}
throw new Error(`Failed createFileAttachment: ${file.originalname} (${file.mimetype} - ${getErrorMessage(error)}`)
}
}
}
return fileAttachments
}