FileCodeBox — 取件/下载次数限制多重绕过(CRITICAL 竞态 + 可复用 URL + IDOR 枚举)
本报告记录了在 main 分支(审计时最新)发现的多个高危/严重漏洞,均已对照当前源码核实。核心是文件分享的"取件次数限制"(expired_count)存在多重可绕过的缺陷:竞态条件(TOCTOU)、可复用下载 URL、过期检查缺失、以及分享码可被枚举。
漏洞 1 — [CRITICAL] TOCTOU 竞态导致取件次数限制完全绕过
位置: apps/base/views.py:214-217(update_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-309(download_file)
select 端点扣减次数并返回 download_url,但 download 端点只校验时间 token(get_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:~209(get_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_file 和 get_code_file 都用默认 check=True,download_file 是唯一例外。文件过期后(时间到期或次数耗尽),在后台清理任务(每 600 秒)物理删除前,攻击者仍可下载。
影响: 已过期文件仍可被下载,违反"过期即不可访问"业务规则。
修复: download_file 使用 check=True(默认值)。
漏洞 4 — [HIGH] 分享码枚举(IDOR)
位置: apps/base/views.py:257-276(get_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 分支源码核实。
FileCodeBox — 取件/下载次数限制多重绕过(CRITICAL 竞态 + 可复用 URL + IDOR 枚举)
本报告记录了在
main分支(审计时最新)发现的多个高危/严重漏洞,均已对照当前源码核实。核心是文件分享的"取件次数限制"(expired_count)存在多重可绕过的缺陷:竞态条件(TOCTOU)、可复用下载 URL、过期检查缺失、以及分享码可被枚举。漏洞 1 — [CRITICAL] TOCTOU 竞态导致取件次数限制完全绕过
位置:
apps/base/views.py:214-217(update_file_usage)+select_file(L292-301)update_file_usage是经典非原子读-改-写:每个并发请求通过
FileCodes.filter(code=...).first()加载自己的副本,修改后save()回写全部字段。N 个并发请求都读到相同的expired_count(如 1),都通过is_expired()检查,都返回文件内容,最终expired_count=0而非负数——典型的丢失更新。影响:
count=1(仅允许取件 1 次)的文件,攻击者并发请求即可获取 N 份,次数限制完全失效。used_count也被低估(每个请求存原始值+1而非累加)。复现:
修复: 用数据库原子操作(
UPDATE ... SET expired_count = expired_count - 1 WHERE expired_count > 0)或SELECT ... FOR UPDATE行锁,确保 check-decrement 原子。漏洞 2 — [HIGH] 可复用下载 URL 绕过次数限制
位置:
apps/base/views.py:305-309(download_file)select端点扣减次数并返回download_url,但download端点只校验时间 token(get_select_token,约 1000 秒窗口),从不调用update_file_usage(不扣次数):一次
select得到的 URL 在 token 窗口内可无限次复用下载,次数限制只在 select 时扣一次。影响:
count=1的文件,攻击者 select 一次后无限下载,次数限制形同虚设。复现:
修复:
download_file也应调用update_file_usage(或在 select 时一次性原子扣减,download 校验已扣减状态)。漏洞 3 — [HIGH] 过期检查缺失(check=False)
位置:
apps/base/views.py:~209(get_code_file_by_code调用)download_file调用get_code_file_by_code时显式传check=False,跳过过期检查:对比
select_file和get_code_file都用默认check=True,download_file是唯一例外。文件过期后(时间到期或次数耗尽),在后台清理任务(每 600 秒)物理删除前,攻击者仍可下载。影响: 已过期文件仍可被下载,违反"过期即不可访问"业务规则。
修复:
download_file使用check=True(默认值)。漏洞 4 — [HIGH] 分享码枚举(IDOR)
位置:
apps/base/views.py:257-276(get_file_metadata)+core/utils.py:get_random_num默认分享码是 5 位数字(10000–99999),仅 90000 种可能。
GET /share/metadata/端点无需认证,且不调用update_file_usage(不扣次数),IP 限流(默认 10 错误/分钟)只在查找失败时计数——成功探测免费。影响: 攻击者枚举整个码空间(约 90000 个),发现所有分享文件(文件名、大小、类型、过期元数据),再
select获取内容。配合多 IP/僵尸网络可快速完成。复现:
修复: 增大分享码空间(如 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。
汇总
views.py:214-217, 292-301views.py:305-309views.py:~209views.py:257-276views.py:download_file所有漏洞均已对照
main分支源码核实。