From 14c506adb34552717fc476c6e6d22de9e8a3b6f4 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 2 Dec 2023 11:10:37 +0530 Subject: [PATCH 1/5] XSS Fixes --- packages/server/package.json | 1 + packages/server/src/index.ts | 430 ++++++++++++++++++++++++----------- 2 files changed, 303 insertions(+), 128 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 6f4ccaf4..f285e663 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -54,6 +54,7 @@ "express": "^4.17.3", "express-basic-auth": "^1.2.1", "express-rate-limit": "^6.9.0", + "express-validator": "^7.0.1", "flowise-components": "*", "flowise-ui": "*", "moment-timezone": "^0.5.34", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d87d2c0a..61f34e92 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -58,6 +58,7 @@ import { CachePool } from './CachePool' import { ICommonObject, IMessage, INodeOptionsValue } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' +import { body, param, query, validationResult } from 'express-validator' export class App { app: express.Application @@ -115,6 +116,9 @@ export class App { // Allow access from * this.app.use(cors()) + // Switch off the default 'X-Powered-By: Express' header + this.app.disable('x-powered-by') + // Add the expressRequestLogger middleware to log all requests this.app.use(expressRequestLogger) @@ -180,17 +184,27 @@ export class App { }) // Get specific component node via name - this.app.get('/api/v1/nodes/:name', (req: Request, res: Response) => { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { + this.app.get('/api/v1/nodes/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { + const name = req.params.name + const result = validationResult(req) + if (!result.isEmpty()) { + throw new Error(`Node ${name} not found`) + } + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { return res.json(this.nodesPool.componentNodes[req.params.name]) } else { - throw new Error(`Node ${req.params.name} not found`) + throw new Error(`Node ${name} not found`) } }) // Get component credential via name - this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => { - if (!req.params.name.includes('&')) { + this.app.get('/api/v1/components-credentials/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { + const name = req.params.name + const result = validationResult(req) + if (!result.isEmpty()) { + throw new Error(`Credential ${name} not found`) + } + if (!req.params.name.includes('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { return res.json(this.nodesPool.componentCredentials[req.params.name]) } else { @@ -198,7 +212,7 @@ export class App { } } else { const returnResponse = [] - for (const name of req.params.name.split('&')) { + for (const name of req.params.name.split('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { returnResponse.push(this.nodesPool.componentCredentials[name]) } else { @@ -210,9 +224,14 @@ export class App { }) // Returns specific component node icon via name - this.app.get('/api/v1/node-icon/:name', (req: Request, res: Response) => { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { - const nodeInstance = this.nodesPool.componentNodes[req.params.name] + this.app.get('/api/v1/node-icon/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { + const name = req.params.name + const result = validationResult(req) + if (!result.isEmpty()) { + throw new Error(`Node ${name} not found`) + } + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { + const nodeInstance = this.nodesPool.componentNodes[name] if (nodeInstance.icon === undefined) { throw new Error(`Node ${req.params.name} icon not found`) } @@ -221,38 +240,48 @@ export class App { const filepath = nodeInstance.icon res.sendFile(filepath) } else { - throw new Error(`Node ${req.params.name} icon is missing icon`) + throw new Error(`Node ${name} icon is missing icon`) } } else { - throw new Error(`Node ${req.params.name} not found`) + throw new Error(`Node ${name} not found`) } }) // Returns specific component credential icon via name - this.app.get('/api/v1/components-credentials-icon/:name', (req: Request, res: Response) => { - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { - const credInstance = this.nodesPool.componentCredentials[req.params.name] + this.app.get('/api/v1/components-credentials-icon/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { + const name = req.params.name + const result = validationResult(req) + if (!result.isEmpty()) { + throw new Error(`Credential ${name} not found`) + } + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { + const credInstance = this.nodesPool.componentCredentials[name] if (credInstance.icon === undefined) { - throw new Error(`Credential ${req.params.name} icon not found`) + throw new Error(`Credential ${name} icon not found`) } if (credInstance.icon.endsWith('.svg') || credInstance.icon.endsWith('.png') || credInstance.icon.endsWith('.jpg')) { const filepath = credInstance.icon res.sendFile(filepath) } else { - throw new Error(`Credential ${req.params.name} icon is missing icon`) + throw new Error(`Credential ${name} is missing icon`) } } else { - throw new Error(`Credential ${req.params.name} not found`) + throw new Error(`Credential ${name} not found`) } }) // load async options - this.app.post('/api/v1/node-load-method/:name', async (req: Request, res: Response) => { + this.app.post('/api/v1/node-load-method/:name', param('name').notEmpty().escape(), async (req: Request, res: Response) => { + const name = req.params.name + const result = validationResult(req) + if (!result.isEmpty()) { + throw new Error(`Node ${name} not found`) + } const nodeData: INodeData = req.body - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { try { - const nodeInstance = this.nodesPool.componentNodes[req.params.name] + const nodeInstance = this.nodesPool.componentNodes[name] const methodName = nodeData.loadMethod || '' const returnOptions: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, { @@ -265,7 +294,7 @@ export class App { return res.json([]) } } else { - res.status(404).send(`Node ${req.params.name} not found`) + res.status(404).send(`Node ${name} not found`) return } }) @@ -281,7 +310,11 @@ export class App { }) // Get specific chatflow via api key - this.app.get('/api/v1/chatflows/apikey/:apiKey', async (req: Request, res: Response) => { + this.app.get('/api/v1/chatflows/apikey/:apiKey', param('apiKey').notEmpty().escape(), async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(401).send('Unauthorized') + } try { const apiKey = await getApiKey(req.params.apiKey) if (!apiKey) return res.status(401).send('Unauthorized') @@ -293,14 +326,19 @@ export class App { .orderBy('cf.name', 'ASC') .getMany() if (chatflows.length >= 1) return res.status(200).send(chatflows) - return res.status(404).send('Chatflow not found') + return res.status(404).send('APIKey not found') } catch (err: any) { return res.status(500).send(err?.message) } }) // Get specific chatflow via id - this.app.get('/api/v1/chatflows/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const chatflowId = req.params.id + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatflow ${chatflowId} not found`) + } const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -309,7 +347,12 @@ export class App { }) // Get specific chatflow via id (PUBLIC endpoint, used when sharing chatbot link) - this.app.get('/api/v1/public-chatflows/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/public-chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const chatflowId = req.params.id + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatflow ${chatflowId} not found`) + } const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -331,48 +374,69 @@ export class App { }) // Update chatflow - this.app.put('/api/v1/chatflows/:id', async (req: Request, res: Response) => { - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id - }) + this.app.put( + '/api/v1/chatflows/:id', + body('chatflow.id').notEmpty(), + param('id').notEmpty().escape(), + async (req: Request, res: Response) => { + const chatflowId = req.params.id + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Chatflow ${chatflowId} not found`) + } + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: chatflowId + }) - if (!chatflow) { - res.status(404).send(`Chatflow ${req.params.id} not found`) - return + if (!chatflow) { + res.status(404).send(`Chatflow ${chatflowId} not found`) + return + } + + const body = req.body + const updateChatFlow = new ChatFlow() + Object.assign(updateChatFlow, body) + + updateChatFlow.id = chatflow.id + createRateLimiter(updateChatFlow) + + this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) + const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) + + // chatFlowPool is initialized only when a flow is opened + // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined + if (this.chatflowPool) { + // Update chatflowpool inSync to false, to build Langchain again because data has been changed + this.chatflowPool.updateInSync(chatflow.id, false) + } + + return res.json(result) } - - const body = req.body - const updateChatFlow = new ChatFlow() - Object.assign(updateChatFlow, body) - - updateChatFlow.id = chatflow.id - createRateLimiter(updateChatFlow) - - this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) - const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) - - // chatFlowPool is initialized only when a flow is opened - // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined - if (this.chatflowPool) { - // Update chatflowpool inSync to false, to build Langchain again because data has been changed - this.chatflowPool.updateInSync(chatflow.id, false) - } - - return res.json(result) - }) + ) // Delete chatflow via id - this.app.delete('/api/v1/chatflows/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const chatflowId = req.params.id + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatflow ${chatflowId} not found`) + } const results = await this.AppDataSource.getRepository(ChatFlow).delete({ id: req.params.id }) return res.json(results) }) // Check if chatflow valid for streaming - this.app.get('/api/v1/chatflows-streaming/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/chatflows-streaming/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const chatflowId = req.params.id + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatflow ${chatflowId} not found`) + } + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: req.params.id + id: chatflowId }) - if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) + if (!chatflow) return res.status(404).send(`Chatflow ${chatflowId} not found`) /*** Get Ending Node with Directed Graph ***/ const flowData = chatflow.flowData @@ -402,58 +466,84 @@ export class App { // ---------------------------------------- // Get all chatmessages from chatflowid - this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { - const sortOrder = req.query?.order as string | undefined - const chatId = req.query?.chatId as string | undefined - const memoryType = req.query?.memoryType as string | undefined - const sessionId = req.query?.sessionId as string | undefined - const startDate = req.query?.startDate as string | undefined - const endDate = req.query?.endDate as string | undefined - let chatTypeFilter = req.query?.chatType as chatType | undefined - - if (chatTypeFilter) { - try { - const chatTypeFilterArray = JSON.parse(chatTypeFilter) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL - } - } catch (e) { - return res.status(500).send(e) + this.app.get( + '/api/v1/chatmessage/:id', + query('chatId').notEmpty().escape(), + query('sortOrder').notEmpty().escape(), + query('memoryType').notEmpty().escape(), + query('sessionId').notEmpty().escape(), + query('startDate').notEmpty().escape(), + query('endDate').notEmpty().escape(), + query('chatTypeFilter').notEmpty().escape(), + async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatmessage not found`) } - } + const sortOrder = req.query?.order as string | undefined + const chatId = req.query?.chatId as string | undefined + const memoryType = req.query?.memoryType as string | undefined + const sessionId = req.query?.sessionId as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + let chatTypeFilter = req.query?.chatType as chatType | undefined - const chatmessages = await this.getChatMessage( - req.params.id, - chatTypeFilter, - sortOrder, - chatId, - memoryType, - sessionId, - startDate, - endDate - ) - return res.json(chatmessages) - }) + if (chatTypeFilter) { + try { + const chatTypeFilterArray = JSON.parse(chatTypeFilter) + if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = undefined + } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { + chatTypeFilter = chatType.EXTERNAL + } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = chatType.INTERNAL + } + } catch (e) { + return res.status(500).send(e) + } + } + + const chatmessages = await this.getChatMessage( + req.params.id, + chatTypeFilter, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate + ) + return res.json(chatmessages) + } + ) // Get internal chatmessages from chatflowid - this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/internal-chatmessage/:id', param('chatId').notEmpty().escape(), async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatmessage not found`) + } const chatmessages = await this.getChatMessage(req.params.id, chatType.INTERNAL) return res.json(chatmessages) }) // Add chatmessages for chatflowid - this.app.post('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { + this.app.post('/api/v1/chatmessage/:id', param('chatId').notEmpty().escape(), async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatmessage not found`) + } const body = req.body const results = await this.addChatMessage(body) return res.json(results) }) // Delete all chatmessages from chatId - this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/chatmessage/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Chatmessage not found`) + } const chatflowid = req.params.id const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowid @@ -537,7 +627,11 @@ export class App { }) // Get specific credential - this.app.get('/api/v1/credentials/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const result = validationResult(req) + if (!result.isEmpty()) { + return res.status(404).send(`Credential ${req.params.id} not found`) + } const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: req.params.id }) @@ -558,7 +652,11 @@ export class App { }) // Update credential - this.app.put('/api/v1/credentials/:id', async (req: Request, res: Response) => { + this.app.put('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Credential ${req.params.id} not found`) + } const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: req.params.id }) @@ -574,7 +672,11 @@ export class App { }) // Delete all chatmessages from chatflowid - this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Credential ${req.params.id} not found`) + } const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id }) return res.json(results) }) @@ -590,7 +692,11 @@ export class App { }) // Get specific tool - this.app.get('/api/v1/tools/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Tool ${req.params.id} not found`) + } const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ id: req.params.id }) @@ -610,7 +716,11 @@ export class App { }) // Update tool - this.app.put('/api/v1/tools/:id', async (req: Request, res: Response) => { + this.app.put('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Tool ${req.params.id} not found`) + } const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ id: req.params.id }) @@ -631,7 +741,11 @@ export class App { }) // Delete tool - this.app.delete('/api/v1/tools/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Tool ${req.params.id} not found`) + } const results = await this.AppDataSource.getRepository(Tool).delete({ id: req.params.id }) return res.json(results) }) @@ -647,7 +761,11 @@ export class App { }) // Get specific assistant - this.app.get('/api/v1/assistants/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Assistant ${req.params.id} not found`) + } const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -655,33 +773,46 @@ export class App { }) // Get assistant object - this.app.get('/api/v1/openai-assistants/:id', async (req: Request, res: Response) => { - const credentialId = req.query.credential as string - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: credentialId - }) + this.app.get( + '/api/v1/openai-assistants/:id', + param('id').notEmpty().escape(), + query('credential').notEmpty().escape(), + async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Assistant or Credential not found`) + } + const credentialId = req.query.credential as string + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) - if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) + if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) - const openAIApiKey = decryptedCredentialData['openAIApiKey'] - if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) - const openai = new OpenAI({ apiKey: openAIApiKey }) - const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) - const resp = await openai.files.list() - const existingFiles = resp.data ?? [] + const openai = new OpenAI({ apiKey: openAIApiKey }) + const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) + const resp = await openai.files.list() + const existingFiles = resp.data ?? [] - if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { - ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) + if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { + ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) + } + + return res.json(retrievedAssistant) } - - return res.json(retrievedAssistant) - }) + ) // List available assistants - this.app.get('/api/v1/openai-assistants', async (req: Request, res: Response) => { + this.app.get('/api/v1/openai-assistants', query('credential').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Assistant or Credential not found`) + } const credentialId = req.query.credential as string const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: credentialId @@ -816,7 +947,11 @@ export class App { }) // Update assistant - this.app.put('/api/v1/assistants/:id', async (req: Request, res: Response) => { + this.app.put('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Assistant ${req.params.id} not found`) + } const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -924,7 +1059,11 @@ export class App { }) // Delete assistant - this.app.delete('/api/v1/assistants/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Assistant ${req.params.id} not found`) + } const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -964,6 +1103,12 @@ export class App { // Download file from assistant this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => { const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName) + //raise error if file path is not absolute + if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`) + //raise error if file path contains '..' + if (filePath.includes('..')) return res.status(500).send(`Invalid file path`) + //only return from the .flowise openai-assistant folder + if (!(filePath.includes('.flowise') && filePath.includes('openai-assistant'))) return res.status(500).send(`Invalid file path`) res.setHeader('Content-Disposition', 'attachment; filename=' + path.basename(filePath)) const fileStream = fs.createReadStream(filePath) fileStream.pipe(res) @@ -973,7 +1118,11 @@ export class App { // Configuration // ---------------------------------------- - this.app.get('/api/v1/flow-config/:id', async (req: Request, res: Response) => { + this.app.get('/api/v1/flow-config/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Chatflow ${req.params.id} not found`) + } const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -1032,7 +1181,11 @@ export class App { } ) - this.app.post('/api/v1/vector/internal-upsert/:id', async (req: Request, res: Response) => { + this.app.post('/api/v1/vector/internal-upsert/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Upsert ${req.params.id} not found`) + } await this.buildChatflow(req, res, undefined, true, true) }) @@ -1043,15 +1196,24 @@ export class App { // Send input message and get prediction result (External) this.app.post( '/api/v1/prediction/:id', + param('id').notEmpty().escape(), upload.array('files'), (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Error Processing Prediction`) + } await this.buildChatflow(req, res, socketIO) } ) // Send input message and get prediction result (Internal) - this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => { + this.app.post('/api/v1/internal-prediction/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Error Processing Prediction`) + } await this.buildChatflow(req, res, socketIO, true) }) @@ -1146,19 +1308,31 @@ export class App { }) // Update api key - this.app.put('/api/v1/apikey/:id', async (req: Request, res: Response) => { + this.app.put('/api/v1/apikey/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Error Processing Update API Key`) + } const keys = await updateAPIKey(req.params.id, req.body.keyName) return addChatflowsCount(keys, res) }) // Delete new api key - this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => { + this.app.delete('/api/v1/apikey/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Error Processing Update API Key`) + } const keys = await deleteAPIKey(req.params.id) return addChatflowsCount(keys, res) }) // Verify api key - this.app.get('/api/v1/verify/apikey/:apiKey', async (req: Request, res: Response) => { + this.app.get('/api/v1/verify/apikey/:apiKey', param('apikey').notEmpty().escape(), async (req: Request, res: Response) => { + const valResult = validationResult(req) + if (!valResult.isEmpty()) { + return res.status(404).send(`Error Processing API Key`) + } try { const apiKey = await getApiKey(req.params.apiKey) if (!apiKey) return res.status(401).send('Unauthorized') From c856d94f7c46aea8428ce649c80feefc7442525d Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Sat, 2 Dec 2023 11:21:06 +0530 Subject: [PATCH 2/5] Dependabot Alerts --- packages/components/package.json | 2 +- packages/server/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index bea9a7a0..a75b9eb5 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -36,7 +36,7 @@ "@upstash/redis": "^1.22.1", "@zilliz/milvus2-sdk-node": "^2.2.24", "apify-client": "^2.7.1", - "axios": "^0.27.2", + "axios": "1.6.2", "cheerio": "^1.0.0-rc.12", "chromadb": "^1.5.11", "cohere-ai": "^6.2.0", diff --git a/packages/server/package.json b/packages/server/package.json index f285e663..38c20389 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -47,7 +47,7 @@ "dependencies": { "@oclif/core": "^1.13.10", "async-mutex": "^0.4.0", - "axios": "^0.27.2", + "axios": "1.6.2", "cors": "^2.8.5", "crypto-js": "^4.1.1", "dotenv": "^16.0.0", From e67c43157a53cc208776431c1fad829f5170d9fd Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Thu, 7 Dec 2023 16:06:32 +0530 Subject: [PATCH 3/5] XSS: Simplified by adding XSS middleware --- packages/server/package.json | 2 +- packages/server/src/index.ts | 421 ++++++++++--------------------- packages/server/src/utils/XSS.ts | 11 + 3 files changed, 142 insertions(+), 292 deletions(-) create mode 100644 packages/server/src/utils/XSS.ts diff --git a/packages/server/package.json b/packages/server/package.json index 38c20389..97a95d43 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -54,7 +54,6 @@ "express": "^4.17.3", "express-basic-auth": "^1.2.1", "express-rate-limit": "^6.9.0", - "express-validator": "^7.0.1", "flowise-components": "*", "flowise-ui": "*", "moment-timezone": "^0.5.34", @@ -64,6 +63,7 @@ "reflect-metadata": "^0.1.13", "socket.io": "^4.6.1", "sqlite3": "^5.1.6", + "strip-js": "^1.2.0", "typeorm": "^0.3.6", "uuid": "^9.0.1", "winston": "^3.9.0" diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 61f34e92..d40b42bf 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -58,7 +58,7 @@ import { CachePool } from './CachePool' import { ICommonObject, IMessage, INodeOptionsValue } from 'flowise-components' import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit' import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey' -import { body, param, query, validationResult } from 'express-validator' +import { sanitizeMiddleware } from './utils/XSS' export class App { app: express.Application @@ -122,6 +122,9 @@ export class App { // Add the expressRequestLogger middleware to log all requests this.app.use(expressRequestLogger) + // Add the sanitizeMiddleware to guard against XSS + this.app.use(sanitizeMiddleware) + if (process.env.FLOWISE_USERNAME && process.env.FLOWISE_PASSWORD) { const username = process.env.FLOWISE_USERNAME const password = process.env.FLOWISE_PASSWORD @@ -184,27 +187,17 @@ export class App { }) // Get specific component node via name - this.app.get('/api/v1/nodes/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Node ${name} not found`) - } - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { + this.app.get('/api/v1/nodes/:name', (req: Request, res: Response) => { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { return res.json(this.nodesPool.componentNodes[req.params.name]) } else { - throw new Error(`Node ${name} not found`) + throw new Error(`Node ${req.params.name} not found`) } }) // Get component credential via name - this.app.get('/api/v1/components-credentials/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Credential ${name} not found`) - } - if (!req.params.name.includes('&')) { + this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => { + if (!req.params.name.includes('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { return res.json(this.nodesPool.componentCredentials[req.params.name]) } else { @@ -212,7 +205,7 @@ export class App { } } else { const returnResponse = [] - for (const name of req.params.name.split('&')) { + for (const name of req.params.name.split('&')) { if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { returnResponse.push(this.nodesPool.componentCredentials[name]) } else { @@ -224,14 +217,9 @@ export class App { }) // Returns specific component node icon via name - this.app.get('/api/v1/node-icon/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Node ${name} not found`) - } - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { - const nodeInstance = this.nodesPool.componentNodes[name] + this.app.get('/api/v1/node-icon/:name', (req: Request, res: Response) => { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { + const nodeInstance = this.nodesPool.componentNodes[req.params.name] if (nodeInstance.icon === undefined) { throw new Error(`Node ${req.params.name} icon not found`) } @@ -240,48 +228,38 @@ export class App { const filepath = nodeInstance.icon res.sendFile(filepath) } else { - throw new Error(`Node ${name} icon is missing icon`) + throw new Error(`Node ${req.params.name} icon is missing icon`) } } else { - throw new Error(`Node ${name} not found`) + throw new Error(`Node ${req.params.name} not found`) } }) // Returns specific component credential icon via name - this.app.get('/api/v1/components-credentials-icon/:name', param('name').notEmpty().escape(), (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Credential ${name} not found`) - } - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) { - const credInstance = this.nodesPool.componentCredentials[name] + this.app.get('/api/v1/components-credentials-icon/:name', (req: Request, res: Response) => { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) { + const credInstance = this.nodesPool.componentCredentials[req.params.name] if (credInstance.icon === undefined) { - throw new Error(`Credential ${name} icon not found`) + throw new Error(`Credential ${req.params.name} icon not found`) } if (credInstance.icon.endsWith('.svg') || credInstance.icon.endsWith('.png') || credInstance.icon.endsWith('.jpg')) { const filepath = credInstance.icon res.sendFile(filepath) } else { - throw new Error(`Credential ${name} is missing icon`) + throw new Error(`Credential ${req.params.name} icon is missing icon`) } } else { - throw new Error(`Credential ${name} not found`) + throw new Error(`Credential ${req.params.name} not found`) } }) // load async options - this.app.post('/api/v1/node-load-method/:name', param('name').notEmpty().escape(), async (req: Request, res: Response) => { - const name = req.params.name - const result = validationResult(req) - if (!result.isEmpty()) { - throw new Error(`Node ${name} not found`) - } + this.app.post('/api/v1/node-load-method/:name', async (req: Request, res: Response) => { const nodeData: INodeData = req.body - if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, name)) { + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) { try { - const nodeInstance = this.nodesPool.componentNodes[name] + const nodeInstance = this.nodesPool.componentNodes[req.params.name] const methodName = nodeData.loadMethod || '' const returnOptions: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, { @@ -294,7 +272,7 @@ export class App { return res.json([]) } } else { - res.status(404).send(`Node ${name} not found`) + res.status(404).send(`Node ${req.params.name} not found`) return } }) @@ -310,11 +288,7 @@ export class App { }) // Get specific chatflow via api key - this.app.get('/api/v1/chatflows/apikey/:apiKey', param('apiKey').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(401).send('Unauthorized') - } + this.app.get('/api/v1/chatflows/apikey/:apiKey', async (req: Request, res: Response) => { try { const apiKey = await getApiKey(req.params.apiKey) if (!apiKey) return res.status(401).send('Unauthorized') @@ -326,19 +300,14 @@ export class App { .orderBy('cf.name', 'ASC') .getMany() if (chatflows.length >= 1) return res.status(200).send(chatflows) - return res.status(404).send('APIKey not found') + return res.status(404).send('Chatflow not found') } catch (err: any) { return res.status(500).send(err?.message) } }) // Get specific chatflow via id - this.app.get('/api/v1/chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } + this.app.get('/api/v1/chatflows/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -347,12 +316,7 @@ export class App { }) // Get specific chatflow via id (PUBLIC endpoint, used when sharing chatbot link) - this.app.get('/api/v1/public-chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } + this.app.get('/api/v1/public-chatflows/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -374,69 +338,48 @@ export class App { }) // Update chatflow - this.app.put( - '/api/v1/chatflows/:id', - body('chatflow.id').notEmpty(), - param('id').notEmpty().escape(), - async (req: Request, res: Response) => { - const chatflowId = req.params.id - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } - const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowId - }) + this.app.put('/api/v1/chatflows/:id', async (req: Request, res: Response) => { + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: req.params.id + }) - if (!chatflow) { - res.status(404).send(`Chatflow ${chatflowId} not found`) - return - } - - const body = req.body - const updateChatFlow = new ChatFlow() - Object.assign(updateChatFlow, body) - - updateChatFlow.id = chatflow.id - createRateLimiter(updateChatFlow) - - this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) - const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) - - // chatFlowPool is initialized only when a flow is opened - // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined - if (this.chatflowPool) { - // Update chatflowpool inSync to false, to build Langchain again because data has been changed - this.chatflowPool.updateInSync(chatflow.id, false) - } - - return res.json(result) + if (!chatflow) { + res.status(404).send(`Chatflow ${req.params.id} not found`) + return } - ) + + const body = req.body + const updateChatFlow = new ChatFlow() + Object.assign(updateChatFlow, body) + + updateChatFlow.id = chatflow.id + createRateLimiter(updateChatFlow) + + this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow) + const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow) + + // chatFlowPool is initialized only when a flow is opened + // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined + if (this.chatflowPool) { + // Update chatflowpool inSync to false, to build Langchain again because data has been changed + this.chatflowPool.updateInSync(chatflow.id, false) + } + + return res.json(result) + }) // Delete chatflow via id - this.app.delete('/api/v1/chatflows/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } + this.app.delete('/api/v1/chatflows/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(ChatFlow).delete({ id: req.params.id }) return res.json(results) }) // Check if chatflow valid for streaming - this.app.get('/api/v1/chatflows-streaming/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const chatflowId = req.params.id - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatflow ${chatflowId} not found`) - } - + this.app.get('/api/v1/chatflows-streaming/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowId + id: req.params.id }) - if (!chatflow) return res.status(404).send(`Chatflow ${chatflowId} not found`) + if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) /*** Get Ending Node with Directed Graph ***/ const flowData = chatflow.flowData @@ -466,84 +409,58 @@ export class App { // ---------------------------------------- // Get all chatmessages from chatflowid - this.app.get( - '/api/v1/chatmessage/:id', - query('chatId').notEmpty().escape(), - query('sortOrder').notEmpty().escape(), - query('memoryType').notEmpty().escape(), - query('sessionId').notEmpty().escape(), - query('startDate').notEmpty().escape(), - query('endDate').notEmpty().escape(), - query('chatTypeFilter').notEmpty().escape(), - async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } - const sortOrder = req.query?.order as string | undefined - const chatId = req.query?.chatId as string | undefined - const memoryType = req.query?.memoryType as string | undefined - const sessionId = req.query?.sessionId as string | undefined - const startDate = req.query?.startDate as string | undefined - const endDate = req.query?.endDate as string | undefined - let chatTypeFilter = req.query?.chatType as chatType | undefined + this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { + const sortOrder = req.query?.order as string | undefined + const chatId = req.query?.chatId as string | undefined + const memoryType = req.query?.memoryType as string | undefined + const sessionId = req.query?.sessionId as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + let chatTypeFilter = req.query?.chatType as chatType | undefined - if (chatTypeFilter) { - try { - const chatTypeFilterArray = JSON.parse(chatTypeFilter) - if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = undefined - } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { - chatTypeFilter = chatType.EXTERNAL - } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { - chatTypeFilter = chatType.INTERNAL - } - } catch (e) { - return res.status(500).send(e) + if (chatTypeFilter) { + try { + const chatTypeFilterArray = JSON.parse(chatTypeFilter) + if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = undefined + } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) { + chatTypeFilter = chatType.EXTERNAL + } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) { + chatTypeFilter = chatType.INTERNAL } + } catch (e) { + return res.status(500).send(e) } - - const chatmessages = await this.getChatMessage( - req.params.id, - chatTypeFilter, - sortOrder, - chatId, - memoryType, - sessionId, - startDate, - endDate - ) - return res.json(chatmessages) } - ) + + const chatmessages = await this.getChatMessage( + req.params.id, + chatTypeFilter, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate + ) + return res.json(chatmessages) + }) // Get internal chatmessages from chatflowid - this.app.get('/api/v1/internal-chatmessage/:id', param('chatId').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } + this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => { const chatmessages = await this.getChatMessage(req.params.id, chatType.INTERNAL) return res.json(chatmessages) }) // Add chatmessages for chatflowid - this.app.post('/api/v1/chatmessage/:id', param('chatId').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } + this.app.post('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { const body = req.body const results = await this.addChatMessage(body) return res.json(results) }) // Delete all chatmessages from chatId - this.app.delete('/api/v1/chatmessage/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Chatmessage not found`) - } + this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => { const chatflowid = req.params.id const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: chatflowid @@ -627,11 +544,7 @@ export class App { }) // Get specific credential - this.app.get('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const result = validationResult(req) - if (!result.isEmpty()) { - return res.status(404).send(`Credential ${req.params.id} not found`) - } + this.app.get('/api/v1/credentials/:id', async (req: Request, res: Response) => { const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: req.params.id }) @@ -652,11 +565,7 @@ export class App { }) // Update credential - this.app.put('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Credential ${req.params.id} not found`) - } + this.app.put('/api/v1/credentials/:id', async (req: Request, res: Response) => { const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: req.params.id }) @@ -672,11 +581,7 @@ export class App { }) // Delete all chatmessages from chatflowid - this.app.delete('/api/v1/credentials/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Credential ${req.params.id} not found`) - } + this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id }) return res.json(results) }) @@ -692,11 +597,7 @@ export class App { }) // Get specific tool - this.app.get('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Tool ${req.params.id} not found`) - } + this.app.get('/api/v1/tools/:id', async (req: Request, res: Response) => { const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ id: req.params.id }) @@ -716,11 +617,7 @@ export class App { }) // Update tool - this.app.put('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Tool ${req.params.id} not found`) - } + this.app.put('/api/v1/tools/:id', async (req: Request, res: Response) => { const tool = await this.AppDataSource.getRepository(Tool).findOneBy({ id: req.params.id }) @@ -741,11 +638,7 @@ export class App { }) // Delete tool - this.app.delete('/api/v1/tools/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Tool ${req.params.id} not found`) - } + this.app.delete('/api/v1/tools/:id', async (req: Request, res: Response) => { const results = await this.AppDataSource.getRepository(Tool).delete({ id: req.params.id }) return res.json(results) }) @@ -761,11 +654,7 @@ export class App { }) // Get specific assistant - this.app.get('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant ${req.params.id} not found`) - } + this.app.get('/api/v1/assistants/:id', async (req: Request, res: Response) => { const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -773,46 +662,33 @@ export class App { }) // Get assistant object - this.app.get( - '/api/v1/openai-assistants/:id', - param('id').notEmpty().escape(), - query('credential').notEmpty().escape(), - async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant or Credential not found`) - } - const credentialId = req.query.credential as string - const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ - id: credentialId - }) + this.app.get('/api/v1/openai-assistants/:id', async (req: Request, res: Response) => { + const credentialId = req.query.credential as string + const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ + id: credentialId + }) - if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) + if (!credential) return res.status(404).send(`Credential ${credentialId} not found`) - // Decrpyt credentialData - const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) - const openAIApiKey = decryptedCredentialData['openAIApiKey'] - if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) + // Decrpyt credentialData + const decryptedCredentialData = await decryptCredentialData(credential.encryptedData) + const openAIApiKey = decryptedCredentialData['openAIApiKey'] + if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`) - const openai = new OpenAI({ apiKey: openAIApiKey }) - const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) - const resp = await openai.files.list() - const existingFiles = resp.data ?? [] + const openai = new OpenAI({ apiKey: openAIApiKey }) + const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id) + const resp = await openai.files.list() + const existingFiles = resp.data ?? [] - if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { - ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) - } - - return res.json(retrievedAssistant) + if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) { + ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id)) } - ) + + return res.json(retrievedAssistant) + }) // List available assistants - this.app.get('/api/v1/openai-assistants', query('credential').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant or Credential not found`) - } + this.app.get('/api/v1/openai-assistants', async (req: Request, res: Response) => { const credentialId = req.query.credential as string const credential = await this.AppDataSource.getRepository(Credential).findOneBy({ id: credentialId @@ -947,11 +823,7 @@ export class App { }) // Update assistant - this.app.put('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant ${req.params.id} not found`) - } + this.app.put('/api/v1/assistants/:id', async (req: Request, res: Response) => { const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -1059,11 +931,7 @@ export class App { }) // Delete assistant - this.app.delete('/api/v1/assistants/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Assistant ${req.params.id} not found`) - } + this.app.delete('/api/v1/assistants/:id', async (req: Request, res: Response) => { const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({ id: req.params.id }) @@ -1118,11 +986,7 @@ export class App { // Configuration // ---------------------------------------- - this.app.get('/api/v1/flow-config/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Chatflow ${req.params.id} not found`) - } + this.app.get('/api/v1/flow-config/:id', async (req: Request, res: Response) => { const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ id: req.params.id }) @@ -1181,11 +1045,7 @@ export class App { } ) - this.app.post('/api/v1/vector/internal-upsert/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Upsert ${req.params.id} not found`) - } + this.app.post('/api/v1/vector/internal-upsert/:id', async (req: Request, res: Response) => { await this.buildChatflow(req, res, undefined, true, true) }) @@ -1196,24 +1056,15 @@ export class App { // Send input message and get prediction result (External) this.app.post( '/api/v1/prediction/:id', - param('id').notEmpty().escape(), upload.array('files'), (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Prediction`) - } await this.buildChatflow(req, res, socketIO) } ) // Send input message and get prediction result (Internal) - this.app.post('/api/v1/internal-prediction/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Prediction`) - } + this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => { await this.buildChatflow(req, res, socketIO, true) }) @@ -1308,31 +1159,19 @@ export class App { }) // Update api key - this.app.put('/api/v1/apikey/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Update API Key`) - } + this.app.put('/api/v1/apikey/:id', async (req: Request, res: Response) => { const keys = await updateAPIKey(req.params.id, req.body.keyName) return addChatflowsCount(keys, res) }) // Delete new api key - this.app.delete('/api/v1/apikey/:id', param('id').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing Update API Key`) - } + this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => { const keys = await deleteAPIKey(req.params.id) return addChatflowsCount(keys, res) }) // Verify api key - this.app.get('/api/v1/verify/apikey/:apiKey', param('apikey').notEmpty().escape(), async (req: Request, res: Response) => { - const valResult = validationResult(req) - if (!valResult.isEmpty()) { - return res.status(404).send(`Error Processing API Key`) - } + this.app.get('/api/v1/verify/apikey/:apiKey', async (req: Request, res: Response) => { try { const apiKey = await getApiKey(req.params.apiKey) if (!apiKey) return res.status(401).send('Unauthorized') diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts new file mode 100644 index 00000000..a69cde21 --- /dev/null +++ b/packages/server/src/utils/XSS.ts @@ -0,0 +1,11 @@ +import { Request, Response, NextFunction } from 'express' +let stripJs = require('strip-js') + +export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { + req.url = stripJs(req.url) + for (let p in req.query) { + req.query[p] = stripJs(req.query[p]) + } + + next() +} From 99bc9d64fbd79aacfd2d488ace0044dcb61fb391 Mon Sep 17 00:00:00 2001 From: vinodkiran Date: Fri, 8 Dec 2023 18:50:58 +0530 Subject: [PATCH 4/5] XSS: replacing deprecated sanitize-js with sanitize-html --- packages/server/package.json | 2 +- packages/server/src/utils/XSS.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 97a95d43..013e6007 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -61,9 +61,9 @@ "mysql": "^2.18.1", "pg": "^8.11.1", "reflect-metadata": "^0.1.13", + "sanitize-html": "^2.11.0", "socket.io": "^4.6.1", "sqlite3": "^5.1.6", - "strip-js": "^1.2.0", "typeorm": "^0.3.6", "uuid": "^9.0.1", "winston": "^3.9.0" diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts index a69cde21..329c2ed2 100644 --- a/packages/server/src/utils/XSS.ts +++ b/packages/server/src/utils/XSS.ts @@ -1,10 +1,12 @@ import { Request, Response, NextFunction } from 'express' -let stripJs = require('strip-js') +const sanitizeHtml = require('sanitize-html') export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { - req.url = stripJs(req.url) + // decoding is necessary as the url is encoded by the browser + const decodedURI = decodeURI(req.url) + req.url = sanitizeHtml(decodedURI) for (let p in req.query) { - req.query[p] = stripJs(req.query[p]) + req.query[p] = sanitizeHtml(req.query[p]) } next() From c06c25b0af75c6a94c7a22bfac8992470d529039 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 9 Dec 2023 14:12:30 +0000 Subject: [PATCH 5/5] add sanitize html types --- packages/server/package.json | 1 + packages/server/src/utils/XSS.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 013e6007..1eeb43f1 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -72,6 +72,7 @@ "@types/cors": "^2.8.12", "@types/crypto-js": "^4.1.1", "@types/multer": "^1.4.7", + "@types/sanitize-html": "^2.9.5", "concurrently": "^7.1.0", "nodemon": "^2.0.15", "oclif": "^3", diff --git a/packages/server/src/utils/XSS.ts b/packages/server/src/utils/XSS.ts index 329c2ed2..3e96e6c8 100644 --- a/packages/server/src/utils/XSS.ts +++ b/packages/server/src/utils/XSS.ts @@ -1,12 +1,12 @@ import { Request, Response, NextFunction } from 'express' -const sanitizeHtml = require('sanitize-html') +import sanitizeHtml from 'sanitize-html' export function sanitizeMiddleware(req: Request, res: Response, next: NextFunction): void { // decoding is necessary as the url is encoded by the browser const decodedURI = decodeURI(req.url) req.url = sanitizeHtml(decodedURI) for (let p in req.query) { - req.query[p] = sanitizeHtml(req.query[p]) + req.query[p] = sanitizeHtml(req.query[p] as string) } next()