Root cause
Path & link handling
feat: support a custom temp_path so temporary files no longer depend on the music_path mount
How AI contributed: New attack surfacePublished May 29, 2026 in
hanxi/xiaomusic
xiaomusic v0.5.7 contains an unauthenticated path traversal vulnerability in the GET /music/{file_path:path} endpoint that allows unauthenticated attackers to read arbitrary files outside the intended music directory by exploiting an incomplete path prefix check. Attackers can request files from sibling directories whose names share the music_path prefix by crafting traversal sequences, bypassing the path restriction due to the missing trailing separator in the comparison logic to retrieve arbitrary files from the server.
The temp-path branch added to music_file() in xiaomusic/api/routers/file.py accepted any path beginning with the configured base, because startswith(temp_base) had no trailing separator to anchor the comparison. A request such as /music/../music_secret/file.txt matched a sibling directory and was served by the unauthenticated GET /music/{file_path:path} route; the fix appends os.sep to the temporary, music and picture bases.
Root cause
feat: support a custom temp_path so temporary files no longer depend on the music_path mount
Fix
fix: add trailing separator to path containment checks (CWE-22) (#891)
Case facts
@@ -957,20 +957,33 @@ async def music_file(request: Request, file_path: str, key: str = "", code: str if not access_key_verification(f"/music/{file_path}", key, code): raise HTTPException(status_code=404, detail="File not found") - absolute_path = os.path.abspath(config.music_path)- absolute_file_path = os.path.normpath(os.path.join(absolute_path, file_path))- if not absolute_file_path.startswith(absolute_path):- raise HTTPException(status_code=404, detail="File not found")- if not os.path.exists(absolute_file_path):- raise HTTPException(status_code=404, detail="File not found")+ # temp/ 前缀表示文件在 temp_path 中+ if file_path.startswith("temp/"):+ temp_file_name = file_path[5:]+ if config.temp_path.startswith("/"):+ temp_base = config.temp_path+ else:+ temp_base = os.path.abspath(config.temp_path)+ absolute_file_path = os.path.normpath(os.path.join(temp_base, temp_file_name))+ if not absolute_file_path.startswith(temp_base):+ raise HTTPException(status_code=404, detail="File not found")+ if not os.path.exists(absolute_file_path):+ raise HTTPException(status_code=404, detail="File not found")+ else:+ absolute_path = os.path.abspath(config.music_path)+ absolute_file_path = os.path.normpath(os.path.join(absolute_path, file_path))+ if not absolute_file_path.startswith(absolute_path):+ raise HTTPException(status_code=404, detail="File not found")+ if not os.path.exists(absolute_file_path):+ raise HTTPException(status_code=404, detail="File not found") # 移除MP3 ID3 v2标签和填充 if config.remove_id3tag and is_mp3(file_path): log.info(f"remove_id3tag:{config.remove_id3tag}, is_mp3:True ") temp_mp3_file = remove_id3_tags(absolute_file_path, config) if temp_mp3_file:- log.info(f"ID3 tag removed {absolute_file_path} to {temp_mp3_file}")- redirect = safe_redirect(f"/music/{temp_mp3_file}")+ mp3_name = os.path.basename(temp_mp3_file)+ redirect = safe_redirect(f"/music/temp/{mp3_name}") if redirect: return redirect else:@@ -979,8 +992,8 @@ async def music_file(request: Request, file_path: str, key: str = "", code: str if config.convert_to_mp3 and not is_mp3(file_path): temp_mp3_file = convert_file_to_mp3(absolute_file_path, config) if temp_mp3_file:- log.info(f"Converted file: {absolute_file_path} to {temp_mp3_file}")- redirect = safe_redirect(f"/music/{temp_mp3_file}")+ mp3_name = os.path.basename(temp_mp3_file)+ redirect = safe_redirect(f"/music/temp/{mp3_name}") if redirect: return redirect else:@@ -965,14 +965,14 @@ async def music_file(request: Request, file_path: str, key: str = "", code: str else: temp_base = os.path.abspath(config.temp_path) absolute_file_path = os.path.normpath(os.path.join(temp_base, temp_file_name))- if not absolute_file_path.startswith(temp_base):+ if not absolute_file_path.startswith(temp_base + os.sep): raise HTTPException(status_code=404, detail="File not found") if not os.path.exists(absolute_file_path): raise HTTPException(status_code=404, detail="File not found") else: absolute_path = os.path.abspath(config.music_path) absolute_file_path = os.path.normpath(os.path.join(absolute_path, file_path))- if not absolute_file_path.startswith(absolute_path):+ if not absolute_file_path.startswith(absolute_path + os.sep): raise HTTPException(status_code=404, detail="File not found") if not os.path.exists(absolute_file_path): raise HTTPException(status_code=404, detail="File not found")@@ -1019,7 +1019,7 @@ async def get_picture(request: Request, file_path: str, key: str = "", code: str absolute_path = os.path.abspath(config.picture_cache_path) absolute_file_path = os.path.normpath(os.path.join(absolute_path, file_path))- if not absolute_file_path.startswith(absolute_path):+ if not absolute_file_path.startswith(absolute_path + os.sep): raise HTTPException(status_code=404, detail="File not found") if not os.path.exists(absolute_file_path): raise HTTPException(status_code=404, detail="File not found")AI-assisted change b94f7c7473e5746598aac2ee0aebb18ca7948d0566f16b52d8316559cc27601e · Fix cc20b37b6f28fd1c7e7fda7025ef2f674bf6655b899a8421ba93c32dbea62900