VYPR
High severityNVD Advisory· Published Jun 2, 2025· Updated Jun 23, 2025

AstrBot Has Path Traversal Vulnerability in /api/chat/get_file

CVE-2025-48957

Description

AstrBot is a large language model chatbot and development framework. A path traversal vulnerability present in versions 3.4.4 through 3.5.12 may lead to information disclosure, such as API keys for LLM providers, account passwords, and other sensitive data. The vulnerability has been addressed in Pull Request #1676 and is included in version 3.5.13. As a workaround, users can edit the cmd_config.json file to disable the dashboard feature as a temporary workaround. However, it is strongly recommended to upgrade to version v3.5.13 or later to fully resolve this issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
astrbotPyPI
>= 3.4.4, < 3.5.133.5.13

Affected products

1

Patches

1
cceadf222c46

Merge pull request #1676 from AstrBotDevs/fix-chat-get-file-bug

https://github.com/AstrBotDevs/AstrBotSoulterMay 29, 2025via ghsa
3 files changed · +116 63
  • astrbot/dashboard/routes/chat.py+23 16 modified
    @@ -61,16 +61,25 @@ async def get_file(self):
                 return Response().error("Missing key: filename").__dict__
     
             try:
    -            with open(os.path.join(self.imgs_dir, filename), "rb") as f:
    -                if filename.endswith(".wav"):
    +            file_path = os.path.join(self.imgs_dir, os.path.basename(filename))
    +            real_file_path = os.path.realpath(file_path)
    +            real_imgs_dir = os.path.realpath(self.imgs_dir)
    +
    +            if not real_file_path.startswith(real_imgs_dir):
    +                return Response().error("Invalid file path").__dict__
    +
    +            with open(real_file_path, "rb") as f:
    +                filename_ext = os.path.splitext(filename)[1].lower()
    +
    +                if filename_ext == ".wav":
                         return QuartResponse(f.read(), mimetype="audio/wav")
    -                elif filename.split(".")[-1] in self.supported_imgs:
    +                elif filename_ext[1:] in self.supported_imgs:
                         return QuartResponse(f.read(), mimetype="image/jpeg")
                     else:
                         return QuartResponse(f.read())
     
    -        except FileNotFoundError:
    -            return Response().error("File not found").__dict__
    +        except (FileNotFoundError, OSError):
    +            return Response().error("File access error").__dict__
     
         async def post_image(self):
             post_data = await request.files
    @@ -126,17 +135,15 @@ async def chat(self):
     
             self.curr_user_cid[username] = conversation_id
     
    -        await web_chat_queue.put(
    -            (
    -                username,
    -                conversation_id,
    -                {
    -                    "message": message,
    -                    "image_url": image_url,  # list
    -                    "audio_url": audio_url,
    -                },
    -            )
    -        )
    +        await web_chat_queue.put((
    +            username,
    +            conversation_id,
    +            {
    +                "message": message,
    +                "image_url": image_url,  # list
    +                "audio_url": audio_url,
    +            },
    +        ))
     
             # 持久化
             conversation = self.db.get_conversation_by_user_id(username, conversation_id)
    
  • astrbot/dashboard/server.py+2 2 modified
    @@ -70,13 +70,13 @@ async def srv_plug_route(self, subpath, *args, **kwargs):
             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
    
  • dashboard/src/views/ChatPage.vue+91 45 modified
    @@ -168,7 +168,7 @@ marked.setOptions({
                                         <template v-slot:activator="{ props }">
                                             <v-btn v-bind="props" @click="sendMessage" class="send-btn" icon="mdi-send"
                                                 variant="text" color="deep-purple"
    -                                            :disabled="!prompt && stagedImagesUrl.length === 0 && !stagedAudioUrl" />
    +                                            :disabled="!prompt && stagedImagesName.length === 0 && !stagedAudioUrl" />
                                         </template>
                                     </v-tooltip>
     
    @@ -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: `<img src="/api/chat/get_file?filename=${img}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
    +                            message: `<img src="${imageUrl}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
                             }
                             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: `<audio controls class="audio-player">
    -                    <source src="/api/chat/get_file?filename=${audio}" type="audio/wav">
    +                    <source src="${audioUrl}" type="audio/wav">
                         您的浏览器不支持音频播放。
                       </audio>`
                             }
    @@ -403,15 +430,14 @@ export default {
                     try {
                         const response = await axios.post('/api/chat/post_file', formData, {
                             headers: {
    -                            'Content-Type': 'multipart/form-data',
    -                            'Authorization': 'Bearer ' + localStorage.getItem('token')
    +                            'Content-Type': 'multipart/form-data'
                             }
                         });
     
                         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);
                     }
    @@ -430,13 +456,13 @@ export default {
                         try {
                             const response = await axios.post('/api/chat/post_image', formData, {
                                 headers: {
    -                                'Content-Type': 'multipart/form-data',
    -                                'Authorization': 'Bearer ' + localStorage.getItem('token')
    +                                'Content-Type': 'multipart/form-data'
                                 }
                             });
     
                             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 +472,7 @@ export default {
             },
     
             removeImage(index) {
    +            this.stagedImagesName.splice(index, 1);
                 this.stagedImagesUrl.splice(index, 1);
             },
     
    @@ -462,28 +489,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 = `<img src="/api/chat/get_file?filename=${img}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
    +                        const imageUrl = await this.getMediaFile(img);
    +                        message[i].message = `<img src="${imageUrl}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
                         }
                         if (message[i].message.startsWith('[RECORD]')) {
                             let audio = message[i].message.replace('[RECORD]', '');
    +                        const audioUrl = await this.getMediaFile(audio);
                             message[i].message = `<audio controls class="audio-player">
    -                                    <source src="/api/chat/get_file?filename=${audio}" type="audio/wav">
    +                                    <source src="${audioUrl}" type="audio/wav">
                                         您的浏览器不支持音频播放。
                                       </audio>`
                         }
                         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,31 +563,40 @@ 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
    -            });
    -
    -            this.scrollToBottom();
    +                image_url: [],
    +                audio_url: null
    +            };
     
    -            // 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);
    +            // 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]);
    +                    }
    +                }
                 }
     
    -            // audio
    -            let audio_filenames = [];
    +            // Convert audio filename to blob URL for display
                 if (this.stagedAudioUrl) {
    -                let audio = this.stagedAudioUrl.replace('/api/chat/get_file?filename=', '');
    -                audio_filenames.push(audio);
    +                if (!this.stagedAudioUrl.startsWith('blob:')) {
    +                    userMessage.audio_url = await this.getMediaFile(this.stagedAudioUrl);
    +                } else {
    +                    userMessage.audio_url = this.stagedAudioUrl;
    +                }
                 }
     
    -            this.loadingChat = true;
    +            this.messages.push(userMessage);
    +            this.scrollToBottom();
     
    +            this.loadingChat = true;
     
                 fetch('/api/chat/send', {
                     method: 'POST',
    @@ -569,20 +607,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 +660,15 @@ export default {
                     }
                 }
             },
    +
    +        cleanupMediaCache() {
    +            Object.values(this.mediaCache).forEach(url => {
    +                if (url.startsWith('blob:')) {
    +                    URL.revokeObjectURL(url);
    +                }
    +            });
    +            this.mediaCache = {};
    +        },
         },
     }
     </script>
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.