11# app/routes/document_routes.py
22import os
3+ from pathlib import Path
34import hashlib
45import traceback
56import aiofiles
67import aiofiles .os
78from shutil import copyfileobj
8- from typing import List , Iterable , TYPE_CHECKING
9+ from typing import List , Iterable , Optional , TYPE_CHECKING
910from concurrent .futures import ThreadPoolExecutor
1011from fastapi import (
1112 APIRouter ,
@@ -109,6 +110,19 @@ def save_upload_file_sync(file: UploadFile, temp_file_path: str) -> None:
109110 )
110111
111112
113+ def validate_file_path (base_dir : str , file_path : str ) -> Optional [str ]:
114+ """Validate that file_path resolves within base_dir. Returns resolved absolute path or None."""
115+ if not file_path or not file_path .strip ():
116+ return None
117+ try :
118+ allowed = Path (base_dir ).resolve ()
119+ requested = Path (os .path .join (base_dir , file_path )).resolve ()
120+ requested .relative_to (allowed )
121+ return str (requested )
122+ except (ValueError , RuntimeError , TypeError , OSError ):
123+ return None
124+
125+
112126async def load_file_content (
113127 filename : str , content_type : str , file_path : str , executor
114128) -> tuple :
@@ -702,8 +716,11 @@ async def store_data_in_vector_db(
702716async def embed_local_file (
703717 document : StoreDocument , request : Request , entity_id : str = None
704718):
705- # Check if the file exists
706- if not os .path .exists (document .filepath ):
719+ file_path = validate_file_path (RAG_UPLOAD_DIR , document .filepath )
720+
721+ # Check if the file exists and if it is within the allowed upload directory
722+ if file_path is None or not os .path .exists (file_path ):
723+ logger .warning ("Path validation failed for local embed: %s" , document .filepath )
707724 raise HTTPException (
708725 status_code = status .HTTP_404_NOT_FOUND ,
709726 detail = ERROR_MESSAGES .FILE_NOT_FOUND ,
@@ -716,7 +733,7 @@ async def embed_local_file(
716733
717734 try :
718735 loader , known_type , file_ext = get_loader (
719- document .filename , document .file_content_type , document . filepath
736+ document .filename , document .file_content_type , file_path
720737 )
721738 data = await run_in_executor (request .app .state .thread_pool , loader .load )
722739
@@ -776,17 +793,26 @@ async def embed_file(
776793 known_type = None
777794
778795 user_id = get_user_id (request , entity_id )
779- temp_base_path = os . path . join ( RAG_UPLOAD_DIR , user_id )
780- os .makedirs ( temp_base_path , exist_ok = True )
781- temp_file_path = os . path . join ( RAG_UPLOAD_DIR , user_id , file . filename )
796+ validated_file_path = validate_file_path (
797+ RAG_UPLOAD_DIR , os .path . join ( user_id , file . filename )
798+ )
782799
783- await save_upload_file_async (file , temp_file_path )
800+ if validated_file_path is None :
801+ logger .warning ("Path validation failed for embed: %s" , file .filename )
802+ raise HTTPException (
803+ status_code = status .HTTP_400_BAD_REQUEST ,
804+ detail = ERROR_MESSAGES .DEFAULT ("Invalid request" ),
805+ )
806+
807+ os .makedirs (os .path .dirname (validated_file_path ), exist_ok = True )
808+
809+ await save_upload_file_async (file , validated_file_path )
784810
785811 try :
786812 data , known_type , file_ext = await load_file_content (
787813 file .filename ,
788814 file .content_type ,
789- temp_file_path ,
815+ validated_file_path ,
790816 request .app .state .thread_pool ,
791817 )
792818
@@ -837,7 +863,7 @@ async def embed_file(
837863 detail = f"Error during file processing: { str (e )} " ,
838864 )
839865 finally :
840- await cleanup_temp_file_async (temp_file_path )
866+ await cleanup_temp_file_async (validated_file_path )
841867
842868 return {
843869 "status" : response_status ,
@@ -904,15 +930,27 @@ async def embed_file_upload(
904930 entity_id : str = Form (None ),
905931):
906932 user_id = get_user_id (request , entity_id )
907- temp_file_path = os .path .join (RAG_UPLOAD_DIR , uploaded_file .filename )
908933
909- await save_upload_file_async (uploaded_file , temp_file_path )
934+ validated_temp_file_path = validate_file_path (
935+ RAG_UPLOAD_DIR , uploaded_file .filename
936+ )
937+
938+ if validated_temp_file_path is None :
939+ logger .warning (
940+ "Path validation failed for embed-upload: %s" , uploaded_file .filename
941+ )
942+ raise HTTPException (
943+ status_code = status .HTTP_400_BAD_REQUEST ,
944+ detail = ERROR_MESSAGES .DEFAULT ("Invalid request" ),
945+ )
946+
947+ await save_upload_file_async (uploaded_file , validated_temp_file_path )
910948
911949 try :
912950 data , known_type , file_ext = await load_file_content (
913951 uploaded_file .filename ,
914952 uploaded_file .content_type ,
915- temp_file_path ,
953+ validated_temp_file_path ,
916954 request .app .state .thread_pool ,
917955 )
918956
@@ -948,7 +986,7 @@ async def embed_file_upload(
948986 detail = f"Error during file processing: { str (e )} " ,
949987 )
950988 finally :
951- await cleanup_temp_file_async (temp_file_path )
989+ await cleanup_temp_file_async (validated_temp_file_path )
952990
953991 return {
954992 "status" : True ,
@@ -1015,17 +1053,27 @@ async def extract_text_from_file(
10151053 Returns the raw text content for text parsing purposes.
10161054 """
10171055 user_id = get_user_id (request , entity_id )
1018- temp_base_path = os . path . join ( RAG_UPLOAD_DIR , user_id )
1019- os .makedirs ( temp_base_path , exist_ok = True )
1020- temp_file_path = os . path . join ( RAG_UPLOAD_DIR , user_id , file . filename )
1056+ validated_temp_file_path = validate_file_path (
1057+ RAG_UPLOAD_DIR , os .path . join ( user_id , file . filename )
1058+ )
10211059
1022- await save_upload_file_async (file , temp_file_path )
1060+ if validated_temp_file_path is None :
1061+ logger .warning ("Path validation failed for text extraction: %s" , file .filename )
1062+ raise HTTPException (
1063+ status_code = status .HTTP_400_BAD_REQUEST ,
1064+ detail = ERROR_MESSAGES .DEFAULT ("Invalid request" ),
1065+ )
1066+
1067+ # create base directory only if the file path is valid
1068+ os .makedirs (os .path .dirname (validated_temp_file_path ), exist_ok = True )
1069+
1070+ await save_upload_file_async (file , validated_temp_file_path )
10231071
10241072 try :
10251073 data , known_type , file_ext = await load_file_content (
10261074 file .filename ,
10271075 file .content_type ,
1028- temp_file_path ,
1076+ validated_temp_file_path ,
10291077 request .app .state .thread_pool ,
10301078 )
10311079
@@ -1064,4 +1112,4 @@ async def extract_text_from_file(
10641112 detail = f"Error during text extraction: { str (e )} " ,
10651113 )
10661114 finally :
1067- await cleanup_temp_file_async (temp_file_path )
1115+ await cleanup_temp_file_async (validated_temp_file_path )
0 commit comments