From 900f14d37cc026d5c9a2eadd71eeaf3ca5457642 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 29 May 2025 19:17:31 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fixed=20a=20potential=20v?= =?UTF-8?q?ulnerability=20in=20/api/chat/get=5Ffile=20endpoint.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I have fixed a potential vulnerability in the `/api/chat/get_file` endpoint that could allow unauthorized access to files by ensuring the request has a jwt token. --- astrbot/dashboard/routes/chat.py | 18 +++- astrbot/dashboard/server.py | 4 +- dashboard/src/views/ChatPage.vue | 136 +++++++++++++++++++++---------- 3 files changed, 109 insertions(+), 49 deletions(-) diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 17e8b115e..b61a60f42 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -60,11 +60,23 @@ class ChatRoute(Route): if not filename: return Response().error("Missing key: filename").__dict__ + # Prevent path traversal attacks by extracting just the basename + filename_ = os.path.basename(filename) + + # Check if the filename contains suspicious patterns + if ( + filename_ != filename + or ".." in filename + or "/" in filename + or "\\" in filename + ): + return Response().error("Invalid filename").__dict__ + try: - with open(os.path.join(self.imgs_dir, filename), "rb") as f: - if filename.endswith(".wav"): + with open(os.path.join(self.imgs_dir, filename_), "rb") as f: + if filename_.endswith(".wav"): return QuartResponse(f.read(), mimetype="audio/wav") - elif filename.split(".")[-1] in self.supported_imgs: + elif filename_.split(".")[-1] in self.supported_imgs: return QuartResponse(f.read(), mimetype="image/jpeg") else: return QuartResponse(f.read()) diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index d8c1a1dd9..acdc8a49f 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -70,13 +70,13 @@ class AstrBotDashboard: for api in registered_web_apis: route, view_handler, methods, _ = api if route == f"/{subpath}" and request.method in methods: - return await view_handler(*args, **kwargs) + return await view_handler(*args, **kwargs) return jsonify(Response().error("未找到该路由").__dict__) async def auth_middleware(self): if not request.path.startswith("/api"): return - allowed_endpoints = ["/api/auth/login", "/api/chat/get_file", "/api/file"] + allowed_endpoints = ["/api/auth/login", "/api/file"] if any(request.path.startswith(prefix) for prefix in allowed_endpoints): return # claim jwt diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index ad149bcd0..da8030105 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -168,7 +168,7 @@ marked.setOptions({ @@ -218,7 +218,8 @@ export default { messages: [], conversations: [], currCid: '', - stagedImagesUrl: [], + stagedImagesName: [], // 用于存储图片**文件名**的数组 + stagedImagesUrl: [], // 用于存储图片的blob URL数组 loadingChat: false, inputFieldLabel: '聊天吧!', @@ -236,7 +237,9 @@ export default { // Ctrl键长按相关变量 ctrlKeyDown: false, ctrlKeyTimer: null, - ctrlKeyLongPressThreshold: 300 // 长按阈值,单位毫秒 + ctrlKeyLongPressThreshold: 300, // 长按阈值,单位毫秒 + + mediaCache: {}, // Add a cache to store media blobs } }, @@ -265,9 +268,31 @@ export default { // 移除keyup事件监听 document.removeEventListener('keyup', this.handleInputKeyUp); + + // Cleanup blob URLs + this.cleanupMediaCache(); }, methods: { + async getMediaFile(filename) { + if (this.mediaCache[filename]) { + return this.mediaCache[filename]; + } + + try { + const response = await axios.get('/api/chat/get_file', { + params: { filename }, + responseType: 'blob' + }); + + const blobUrl = URL.createObjectURL(response.data); + this.mediaCache[filename] = blobUrl; + return blobUrl; + } catch (error) { + console.error('Error fetching media file:', error); + return ''; + } + }, async startListeningEvent() { const response = await fetch('/api/chat/listen', { @@ -328,17 +353,19 @@ export default { if (chunk_json.type === 'image') { let img = chunk_json.data.replace('[IMAGE]', ''); + const imageUrl = await this.getMediaFile(img); let bot_resp = { type: 'bot', - message: `` + message: `` } this.messages.push(bot_resp); } else if (chunk_json.type === 'record') { let audio = chunk_json.data.replace('[RECORD]', ''); + const audioUrl = await this.getMediaFile(audio); let bot_resp = { type: 'bot', message: `` } @@ -411,7 +438,7 @@ export default { const audio = response.data.data.filename; console.log('Audio uploaded:', audio); - this.stagedAudioUrl = `/api/chat/get_file?filename=${audio}`; + this.stagedAudioUrl = audio; // Store just the filename } catch (err) { console.error('Error uploading audio:', err); } @@ -436,7 +463,8 @@ export default { }); const img = response.data.data.filename; - this.stagedImagesUrl.push(`/api/chat/get_file?filename=${img}`); + this.stagedImagesName.push(img); // Store just the filename + this.stagedImagesUrl.push(URL.createObjectURL(file)); // Create a blob URL for immediate display } catch (err) { console.error('Error uploading image:', err); @@ -446,6 +474,7 @@ export default { }, removeImage(index) { + this.stagedImagesName.splice(index, 1); this.stagedImagesUrl.splice(index, 1); }, @@ -462,28 +491,30 @@ export default { getConversationMessages(cid) { if (!cid[0]) return; - axios.get('/api/chat/get_conversation?conversation_id=' + cid[0]).then(response => { + axios.get('/api/chat/get_conversation?conversation_id=' + cid[0]).then(async response => { this.currCid = cid[0]; let message = JSON.parse(response.data.data.history); for (let i = 0; i < message.length; i++) { if (message[i].message.startsWith('[IMAGE]')) { let img = message[i].message.replace('[IMAGE]', ''); - message[i].message = `` + const imageUrl = await this.getMediaFile(img); + message[i].message = `` } if (message[i].message.startsWith('[RECORD]')) { let audio = message[i].message.replace('[RECORD]', ''); + const audioUrl = await this.getMediaFile(audio); message[i].message = `` } if (message[i].image_url && message[i].image_url.length > 0) { for (let j = 0; j < message[i].image_url.length; j++) { - message[i].image_url[j] = `/api/chat/get_file?filename=${message[i].image_url[j]}`; + message[i].image_url[j] = await this.getMediaFile(message[i].image_url[j]); } } if (message[i].audio_url) { - message[i].audio_url = `/api/chat/get_file?filename=${message[i].audio_url}`; + message[i].audio_url = await this.getMediaFile(message[i].audio_url); } } this.messages = message; @@ -534,32 +565,41 @@ export default { await this.newConversation(); } - this.messages.push({ + // Create a message object with actual URLs for display + const userMessage = { type: 'user', message: this.prompt, - image_url: this.stagedImagesUrl, - audio_url: this.stagedAudioUrl - }); + image_url: [], + audio_url: null + }; + // Convert image filenames to blob URLs for display + if (this.stagedImagesName.length > 0) { + for (let i = 0; i < this.stagedImagesName.length; i++) { + // If it's just a filename, get the blob URL + if (!this.stagedImagesName[i].startsWith('blob:')) { + const imgUrl = await this.getMediaFile(this.stagedImagesName[i]); + userMessage.image_url.push(imgUrl); + } else { + userMessage.image_url.push(this.stagedImagesName[i]); + } + } + } + + // Convert audio filename to blob URL for display + if (this.stagedAudioUrl) { + if (!this.stagedAudioUrl.startsWith('blob:')) { + userMessage.audio_url = await this.getMediaFile(this.stagedAudioUrl); + } else { + userMessage.audio_url = this.stagedAudioUrl; + } + } + + this.messages.push(userMessage); this.scrollToBottom(); - // images - let image_filenames = []; - for (let i = 0; i < this.stagedImagesUrl.length; i++) { - let img = this.stagedImagesUrl[i].replace('/api/chat/get_file?filename=', ''); - image_filenames.push(img); - } - - // audio - let audio_filenames = []; - if (this.stagedAudioUrl) { - let audio = this.stagedAudioUrl.replace('/api/chat/get_file?filename=', ''); - audio_filenames.push(audio); - } - this.loadingChat = true; - fetch('/api/chat/send', { method: 'POST', headers: { @@ -569,20 +609,19 @@ export default { body: JSON.stringify({ message: this.prompt, conversation_id: this.currCid, - image_url: image_filenames, - audio_url: audio_filenames - }) // 发送请求体 - }) - .then(response => { - this.prompt = ''; - this.stagedImagesUrl = []; - this.stagedAudioUrl = ""; - - this.loadingChat = false; + image_url: this.stagedImagesName, // Already contains just filenames + audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : [] // Already contains just filename }) - .catch(err => { - console.error(err); - }); + }) + .then(response => { + this.prompt = ''; + this.stagedImagesName = []; + this.stagedAudioUrl = ""; + this.loadingChat = false; + }) + .catch(err => { + console.error(err); + }); }, scrollToBottom() { this.$nextTick(() => { @@ -623,6 +662,15 @@ export default { } } }, + + cleanupMediaCache() { + Object.values(this.mediaCache).forEach(url => { + if (url.startsWith('blob:')) { + URL.revokeObjectURL(url); + } + }); + this.mediaCache = {}; + }, }, }