| 1 | import os |
| 2 | |
| 3 | |
| 4 | def resolve_path_within_directory( |
| 5 | base_dir: str, |
| 6 | unsafe_path: str, |
| 7 | *, |
| 8 | require_file: bool = True, |
| 9 | ) -> str: |
| 10 | # 用户传入的路径可能是文件名、相对路径、绝对路径,也可能夹带 `../`。 |
| 11 | # 这里统一解析成真实路径,并用 commonpath 判断它是否仍在允许目录内。 |
| 12 | # 这样比简单判断字符串前缀可靠,可以覆盖符号链接、重复分隔符、相对路径 |
| 13 | # 等场景,适用于上传目录、素材目录、任务产物目录这类白名单目录。 |
| 14 | if not unsafe_path: |
| 15 | raise ValueError("empty path is not allowed") |
| 16 | |
| 17 | base_dir_real = os.path.realpath(base_dir) |
| 18 | candidate_path = unsafe_path |
| 19 | if not os.path.isabs(candidate_path): |
| 20 | candidate_path = os.path.join(base_dir_real, candidate_path) |
| 21 | |
| 22 | resolved_path = os.path.realpath(candidate_path) |
| 23 | try: |
| 24 | common_path = os.path.commonpath([base_dir_real, resolved_path]) |
| 25 | except ValueError as exc: |
| 26 | # Windows 下不同盘符会触发 ValueError,这类路径一定不属于允许目录。 |
| 27 | raise ValueError("path is outside the allowed directory") from exc |
| 28 | |
| 29 | if common_path != base_dir_real: |
| 30 | raise ValueError("path is outside the allowed directory") |
| 31 | |
| 32 | if require_file and not os.path.isfile(resolved_path): |
| 33 | raise ValueError("file does not exist") |
| 34 | |
| 35 | return resolved_path |
| 36 |