Skip to content

[安全] 取件次数限制多重绕过(CRITICAL 竞态 + 可复用 URL + 过期检查缺失 + 分享码枚举) #482

@Galaxync

Description

@Galaxync

FileCodeBox — 取件/下载次数限制多重绕过(CRITICAL 竞态 + 可复用 URL + IDOR 枚举)

本报告记录了在 main 分支(审计时最新)发现的多个高危/严重漏洞,均已对照当前源码核实。核心是文件分享的"取件次数限制"(expired_count)存在多重可绕过的缺陷:竞态条件(TOCTOU)、可复用下载 URL、过期检查缺失、以及分享码可被枚举。


漏洞 1 — [CRITICAL] TOCTOU 竞态导致取件次数限制完全绕过

位置: apps/base/views.py:214-217update_file_usage)+ select_file(L292-301)

update_file_usage 是经典非原子读-改-写:

async def update_file_usage(file_code: FileCodes) -> None:
    if file_code.expired_count > 0:      # 读
        file_code.expired_count -= 1     # 改(本地副本)
        await file_code.save()           # 写(全字段回写)

每个并发请求通过 FileCodes.filter(code=...).first() 加载自己的副本,修改后 save() 回写全部字段。N 个并发请求都读到相同的 expired_count(如 1),都通过 is_expired() 检查,都返回文件内容,最终 expired_count=0 而非负数——典型的丢失更新。

影响: count=1(仅允许取件 1 次)的文件,攻击者并发请求即可获取 N 份,次数限制完全失效。used_count 也被低估(每个请求存 原始值+1 而非累加)。

复现:

# count=1 的分享,并发 10 个 select 请求:
for i in $(seq 1 10); do
  curl -s -X POST "http://<host>/share/select/" \
       -H "Content-Type: application/json" \
       -d '{"code":"<分享码>"}' &
done; wait
# 所有请求都返回文件内容,expired_count 最终=0,used_count=1(应为 10)。

修复: 用数据库原子操作(UPDATE ... SET expired_count = expired_count - 1 WHERE expired_count > 0)或 SELECT ... FOR UPDATE 行锁,确保 check-decrement 原子。


漏洞 2 — [HIGH] 可复用下载 URL 绕过次数限制

位置: apps/base/views.py:305-309download_file

select 端点扣减次数并返回 download_url,但 download 端点只校验时间 tokenget_select_token,约 1000 秒窗口),从不调用 update_file_usage(不扣次数):

@share_api.get("/download")
async def download_file(key: str, code: str, ...):
    if await get_select_token(normalized_code) != key:   # 只校验时间 token
        ...
    # 无 update_file_usage 调用,无 is_expired 检查

一次 select 得到的 URL 在 token 窗口内可无限次复用下载,次数限制只在 select 时扣一次。

影响: count=1 的文件,攻击者 select 一次后无限下载,次数限制形同虚设。

复现:

curl -X POST "http://<host>/share/select/" -d '{"code":"<码>"}'   # 扣 1 次,得 download_url
# 在 ~16.7 分钟内反复用同一 URL 下载:
for i in $(seq 1 100); do curl -s -o /dev/null "http://<host>/share/download?key=abc&code=12345"; done

修复: download_file 也应调用 update_file_usage(或在 select 时一次性原子扣减,download 校验已扣减状态)。


漏洞 3 — [HIGH] 过期检查缺失(check=False)

位置: apps/base/views.py:~209get_code_file_by_code 调用)

download_file 调用 get_code_file_by_code 时显式传 check=False,跳过过期检查:

if await file_code.is_expired() and check:   # check=False 时整段跳过
    return False, "文件已过期"

对比 select_fileget_code_file 都用默认 check=Truedownload_file 是唯一例外。文件过期后(时间到期或次数耗尽),在后台清理任务(每 600 秒)物理删除前,攻击者仍可下载。

影响: 已过期文件仍可被下载,违反"过期即不可访问"业务规则。

修复: download_file 使用 check=True(默认值)。


漏洞 4 — [HIGH] 分享码枚举(IDOR)

位置: apps/base/views.py:257-276get_file_metadata)+ core/utils.py:get_random_num

默认分享码是 5 位数字(10000–99999),仅 90000 种可能。GET /share/metadata/ 端点无需认证,且不调用 update_file_usage(不扣次数),IP 限流(默认 10 错误/分钟)只在查找失败时计数——成功探测免费

影响: 攻击者枚举整个码空间(约 90000 个),发现所有分享文件(文件名、大小、类型、过期元数据),再 select 获取内容。配合多 IP/僵尸网络可快速完成。

复现:

for code in $(seq 10000 99999); do
  curl -s "http://<host>/share/metadata/?code=$code" | grep -q detail && echo "HIT: $code"
done
# 每个 200 响应泄露文件元数据;404 才消耗限流配额。

修复: 增大分享码空间(如 10 位 base36);metadata 端点也应计入限流(成功也计数);必要时要求认证。


漇洞 5 — [HIGH] 次数限制绕过(状态机缺失)

位置: apps/base/views.py:download_file + update_file_usage

与漏洞 2/3 同源:次数扣减只在 select 端点,download 不扣;get_select_token 基于 int(time.time()/1000) 在 ~1000 秒窗口内不变,单次 select 即可无限下载。这是次数限制"状态机缺失"的业务逻辑表述。

修复: 同漏洞 2。


汇总

# 漏洞 严重度 位置
1 TOCTOU 竞态绕过取件次数 CRITICAL views.py:214-217, 292-301
2 可复用下载 URL 绕过次数 HIGH views.py:305-309
3 过期检查缺失(check=False) HIGH views.py:~209
4 分享码枚举(IDOR) HIGH views.py:257-276
5 次数限制状态机缺失 HIGH views.py:download_file

所有漏洞均已对照 main 分支源码核实。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions