Skip to content

fix(uploads): add Windows support for safe symlink-protected uploads#2794

Open
p-yf wants to merge 1 commit intobytedance:mainfrom
p-yf:fix/uploads-manager-windows-support-
Open

fix(uploads): add Windows support for safe symlink-protected uploads#2794
p-yf wants to merge 1 commit intobytedance:mainfrom
p-yf:fix/uploads-manager-windows-support-

Conversation

@p-yf
Copy link
Copy Markdown

@p-yf p-yf commented May 8, 2026

Fixes #2793

Summary

windows无法上传任何文件,增加对于windows的文件上传支持

问题原因

windows 没有 O_NOFOLLOW,导致 在deerflow.uploads.manager.open_upload_file_no_symlink的代码直接抛出异常:

if not hasattr(os, "O_NOFOLLOW"):
    raise UnsafeUploadPathError("Upload writes require O_NOFOLLOW support")

这使得 Windows 上的文件上传功能完全不可用。

##解决方案

  • 移除上述代码直接拒绝逻辑
  • 保持条件满足 hasattr(os, "O_NOFOLLOW")的逻辑不变
  • 增加当条件为not hasattr(os, "O_NOFOLLOW")的处理,逻辑与前一个分支大致相同

##结果
前端接口显示:
image
后端文件存储:
image

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 8, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_NOFOLLOW is missing) with a Windows-compatible open flow.
  • Adds a Windows-specific open branch using lstat pre-checks plus fstat validation 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.
@WillemJiang
Copy link
Copy Markdown
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

cd backend
make format

@WillemJiang WillemJiang added the reviewing The PR is in reviewing status label May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

reviewing The PR is in reviewing status

Projects

None yet

Development

Successfully merging this pull request may close these issues.

windows无法上传文件

4 participants