fix(project_file): path/project_id循环unquote兼容前端双重编码——bricks.tget对已编码query值再encodeURIComponent一次(双重编码),服务端只解一层残留%XX编码态中文→项目名_ident_ok校验失败误报'非法路径'(2026-09-16投标标书md在线查看实测根因;下载走window.open单层编码不受影响,故只有在线查看中招)。对齐workspace_view.dspy的_decode_id循环解码到无%。共享端点:task_io/todo_detail/workspace全部md在线查看一并根治

This commit is contained in:
ymq 2026-09-16 15:13:37 +08:00
parent 28a26dc01f
commit f05d95b8ac

View File

@ -4,15 +4,32 @@
# 安全:仅登录用户;解析后校验文件真实路径必须落在该项目目录内(防路径穿越/跨项目越权)。
import os
from urllib.parse import quote
from urllib.parse import quote, unquote
from aiohttp.web_fileresponse import FileResponse
def _decode_path(s):
# 前端 bricks.tget 会对已编码的 query 值再 encodeURIComponent 一次(双重编码),
# 服务端框架只解一层 → 残留 %XX 编码态中文 → 项目名/文件名校验失败,
# 误报「非法路径」或「文件不存在」2026-09-16 投标标书 md 在线查看实测根因;
# 下载按钮走 window.open 单层编码不受影响,故只有「在线查看」中招)。
# 循环 unquote 到无 % 为止,兼容任意编码层数(对齐 workspace_view.dspy 的 _decode_id
for _ in range(4):
if '%' not in s:
break
s2 = unquote(s)
if s2 == s:
break
s = s2
return s
uid = await get_user()
if not uid:
return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}}
project_id = ((params_kw or {}).get('project_id') or '').strip()
fpath = ((params_kw or {}).get('path') or '').strip()
project_id = _decode_path(((params_kw or {}).get('project_id') or '').strip())
fpath = _decode_path(((params_kw or {}).get('path') or '').strip())
download = ((params_kw or {}).get('download') or '').strip()
if not project_id: