fix(uploads): add Windows support for safe symlink-protected uploads#2794
Open
p-yf wants to merge 1 commit intobytedance:mainfrom
Open
fix(uploads): add Windows support for safe symlink-protected uploads#2794p-yf wants to merge 1 commit intobytedance:mainfrom
p-yf wants to merge 1 commit intobytedance:mainfrom
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR aims to restore file upload functionality on Windows by adding a fallback path in open_upload_file_no_symlink when os.O_NOFOLLOW is not available, while keeping the existing POSIX O_NOFOLLOW behavior unchanged.
Changes:
- Replaces the previous “fail closed” behavior (when
O_NOFOLLOWis missing) with a Windows-compatible open flow. - Adds a Windows-specific open branch using
lstatpre-checks plusfstatvalidation after opening. - Updates the function docstring/comments to describe the Windows behavior.
Comment on lines
+171
to
199
| # Windows 分支:系统没有 O_NOFOLLOW,改用"双重 lstat 检查 + 事后 fstat 检查" | ||
| if st is not None and st.st_nlink > 1: # 硬链接数量 > 1 → 拒绝 | ||
| raise UnsafeUploadPathError(f"Upload destination has multiple links: {safe_name}") | ||
|
|
||
| flags = os.O_WRONLY | os.O_CREAT | ||
|
|
||
| # Windows 默认文本模式会做 \n ↔ \r\n 自动转换,破坏二进制文件 | ||
| # O_BINARY 告诉系统不要做任何换行符转换 | ||
| if hasattr(os, "O_BINARY"): | ||
| flags |= os.O_BINARY | ||
| try: | ||
| pre_open_st = os.lstat(dest) | ||
| except FileNotFoundError: | ||
| pre_open_st = None | ||
|
|
||
| flags = os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW | ||
| if hasattr(os, "O_NONBLOCK"): | ||
| flags |= os.O_NONBLOCK | ||
| # 如果文件存在,必须是普通文件(不是 symlink/目录/设备) | ||
| if pre_open_st is not None and not stat.S_ISREG(pre_open_st.st_mode): | ||
| raise UnsafeUploadPathError(f"Upload destination is not a regular file: {safe_name}") | ||
| # 硬链接数量 > 1 → 拒绝(truncate 会影响其他文件名) | ||
| if pre_open_st is not None and pre_open_st.st_nlink > 1: | ||
| raise UnsafeUploadPathError(f"Upload destination has multiple links: {safe_name}") | ||
|
|
||
| try: | ||
| fd = os.open(dest, flags, 0o600) | ||
| except OSError as exc: | ||
| if exc.errno in {errno.ELOOP, errno.EISDIR, errno.ENOTDIR, errno.ENXIO, errno.EAGAIN}: | ||
| raise UnsafeUploadPathError(f"Unsafe upload destination: {safe_name}") from exc | ||
| raise | ||
| fd = os.open(dest, flags, 0o600) # 以 0o600 权限打开/创建文件 | ||
|
|
||
| # 这就是用户建议的"先打开、再检查、再清空"流程! | ||
| try: | ||
| opened_stat = os.fstat(fd) | ||
| if not stat.S_ISREG(opened_stat.st_mode) or opened_stat.st_nlink != 1: | ||
| opened_stat = os.fstat(fd) # 通过 fd 获取打开后的真实 inode 属性 | ||
| if not stat.S_ISREG(opened_stat.st_mode) or opened_stat.st_nlink > 1: | ||
| raise UnsafeUploadPathError(f"Upload destination is not an exclusive regular file: {safe_name}") |
| if exc.errno in {errno.ELOOP, errno.EISDIR, errno.ENOTDIR, errno.ENXIO, errno.EAGAIN}: | ||
| raise UnsafeUploadPathError(f"Unsafe upload destination: {safe_name}") from exc | ||
| raise | ||
| fd = os.open(dest, flags, 0o600) # 以 0o600 权限打开/创建文件 |
Comment on lines
+142
to
+156
| has_nofollow = hasattr(os, "O_NOFOLLOW") | ||
|
|
||
| # 原逻辑:利用 O_NOFOLLOW 在 open() 时拒绝 symlink | ||
| if has_nofollow: | ||
| flags = os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW | ||
| if hasattr(os, "O_NONBLOCK"): | ||
| flags |= os.O_NONBLOCK | ||
|
|
||
| try: | ||
| fd = os.open(dest, flags, 0o600) | ||
| except OSError as exc: | ||
| if exc.errno in {errno.ELOOP, errno.EISDIR, errno.ENOTDIR, errno.ENXIO, errno.EAGAIN}: | ||
| raise UnsafeUploadPathError(f"Unsafe upload destination: {safe_name}") from exc | ||
| raise | ||
|
|
Comment on lines
+129
to
136
| safe_name = normalize_filename(filename) # 去除路径遍历字符,得到安全的文件名 | ||
| dest = base_dir / safe_name # 拼接到上传目录,得到完整目标路径 | ||
|
|
||
| try: | ||
| st = os.lstat(dest) | ||
| st = os.lstat(dest) # 获取文件属性(不跟随 symlink) | ||
| except FileNotFoundError: | ||
| st = None | ||
| st = None # 文件不存在,st 为 None,后续直接创建 | ||
|
|
Comment on lines
+126
to
+127
| ``O_NOFOLLOW`` (Windows lacks this flag), relying on the pre-check to catch | ||
| existing symlinks and path-traversal validation to prevent escapes. |
Collaborator
|
@p-yf thanks for your contribution. Please translate the code comments into English and add unit tests to avoid regressions. BTW, you can fix the lint error by running the below command |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2793
Summary
windows无法上传任何文件,增加对于windows的文件上传支持
问题原因
windows 没有
O_NOFOLLOW,导致 在deerflow.uploads.manager.open_upload_file_no_symlink的代码直接抛出异常:这使得 Windows 上的文件上传功能完全不可用。
##解决方案
##结果


前端接口显示:
后端文件存储: