@@ -111,6 +111,17 @@ def __init__(self):
111111 self .chunk_size = 256 * 1024
112112 self .root_path = data_root
113113
114+ def _resolve_safe_path (self , relative_path : str ) -> Path :
115+ """将相对路径解析到数据根目录内,阻止路径穿越。"""
116+ root = self .root_path .resolve ()
117+ raw = str (relative_path or "" ).replace ("\\ " , "/" ).lstrip ("/" )
118+ candidate = (root / raw ).resolve ()
119+ try :
120+ candidate .relative_to (root )
121+ except ValueError as exc :
122+ raise ValueError ("非法文件路径" ) from exc
123+ return candidate
124+
114125 def _save (self , file , save_path ):
115126 with open (save_path , "wb" ) as f :
116127 chunk = file .read (self .chunk_size )
@@ -119,27 +130,27 @@ def _save(self, file, save_path):
119130 chunk = file .read (self .chunk_size )
120131
121132 async def save_file (self , file : UploadFile , save_path : str ):
122- path_obj = Path (save_path )
123- directory = str (path_obj .parent )
133+ path_obj = Path (str ( save_path ). replace ( " \\ " , "/" ) )
134+ directory = str (path_obj .parent ). replace ( " \\ " , "/" ). lstrip ( "/" )
124135 # 提取原始文件名并进行清理
125136 filename = await sanitize_filename (path_obj .name )
126137 # 构建安全的完整保存路径
127- safe_save_path = self .root_path / directory / filename
138+ safe_save_path = self ._resolve_safe_path ( f" { directory } / { filename } " if directory not in { "" , "." } else filename )
128139 # 确保目录存在
129140 if not safe_save_path .parent .exists ():
130141 safe_save_path .parent .mkdir (parents = True )
131142 await asyncio .to_thread (self ._save , file .file , safe_save_path )
132143
133144 async def delete_file (self , file_code : FileCodes ):
134- save_path = self .root_path / await file_code .get_file_path ()
145+ save_path = self ._resolve_safe_path ( await file_code .get_file_path () )
135146 if save_path .exists ():
136147 save_path .unlink ()
137148
138149 async def get_file_url (self , file_code : FileCodes ):
139150 return await get_file_url (file_code .code )
140151
141152 async def get_file_response (self , file_code : FileCodes ):
142- file_path = self .root_path / await file_code .get_file_path ()
153+ file_path = self ._resolve_safe_path ( await file_code .get_file_path () )
143154 if not file_path .exists ():
144155 return APIResponse (code = 404 , detail = "文件已过期删除" )
145156 filename = f"{ file_code .prefix } { file_code .suffix } "
@@ -171,8 +182,11 @@ async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes,
171182 :param chunk_hash: 分片哈希值
172183 :param save_path: 文件保存路径
173184 """
174- chunk_dir = self .root_path / save_path
175- chunk_path = chunk_dir .parent / 'chunks' / upload_id / f"{ chunk_index } .part"
185+ # 先校验目标文件路径合法,再将分片落到同级 chunks 目录。
186+ self ._resolve_safe_path (save_path )
187+ chunk_path = self ._resolve_safe_path (
188+ str (Path (save_path ).parent / "chunks" / upload_id / f"{ chunk_index } .part" )
189+ )
176190 if not chunk_path .parent .exists ():
177191 chunk_path .parent .mkdir (parents = True , exist_ok = True )
178192 # 使用临时文件写入,确保原子性
@@ -195,9 +209,11 @@ async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path:
195209 :param save_path: 文件保存路径
196210 :return: (文件路径, 文件哈希值)
197211 """
198- output_path = self .root_path / save_path
212+ output_path = self ._resolve_safe_path ( save_path )
199213 output_path .parent .mkdir (parents = True , exist_ok = True )
200- chunk_base_dir = output_path .parent / 'chunks' / upload_id
214+ chunk_base_dir = self ._resolve_safe_path (
215+ str (Path (save_path ).parent / "chunks" / upload_id )
216+ )
201217 file_sha256 = hashlib .sha256 ()
202218
203219 # 使用临时文件写入,确保原子性
@@ -233,7 +249,9 @@ async def clean_chunks(self, upload_id: str, save_path: str):
233249 :param upload_id: 上传会话ID
234250 :param save_path: 文件保存路径
235251 """
236- chunk_dir = (self .root_path / save_path ).parent / 'chunks' / upload_id
252+ chunk_dir = self ._resolve_safe_path (
253+ str (Path (save_path ).parent / "chunks" / upload_id )
254+ )
237255 if chunk_dir .exists ():
238256 try :
239257 shutil .rmtree (chunk_dir )
@@ -253,7 +271,10 @@ async def file_exists(self, save_path: str) -> bool:
253271 :param save_path: 文件路径
254272 :return: 文件是否存在
255273 """
256- file_path = self .root_path / save_path
274+ try :
275+ file_path = self ._resolve_safe_path (save_path )
276+ except ValueError :
277+ return False
257278 return file_path .exists ()
258279
259280
0 commit comments