2026-09-09-attachment-access-control-design.md 7.9 KB

附件访问控制设计(纪检隐私)

日期: 2026-09-09
状态: 待实现
范围: backend、web-admin、mobile-wx

1. 背景与目标

纪检业务附件含隐私。当前 MinIO bucket ajyy 若整桶匿名可读,知链即可下载,不满足安全要求。

目标:

对象 访问规则
二维码配置图片(qrcode/*) 允许匿名可读(公示张贴、移动端展示)
在线反馈 / 建言献策附件 禁止匿名读
web-admin 必须登录;经后端 JWT 代理拉流查看/下载
mobile-wx 仅上传界面用本地临时文件预览;不依赖远程可读

2. 已定决策

  • 管理端访问方式:后端鉴权代理(非预签名直链)
  • Bucket 策略:同一 bucket,按前缀分流(非双 bucket)
  • 库表:继续存现有 url / objectKey 形态,不迁表
  • 威胁模型(明确): 公开上传接口可被任意客户端调用(无法绑定「仅官方小程序」);私有附件不可匿名读——防泄漏靠读权限,不靠「假装只有移动端能上传」
  • 上传内容安全: 公开(及管理端)上传的文件按证据/材料存储与下载,服务端不执行;并通过类型白名单与响应头降低木马/恶意脚本被误执行或浏览器执行的风险

3. 方案对比(摘要)

方案 说明 结论
1(采用) qrcode/* 公开;其余私有;管理端 JWT 代理;移动端本地预览 采用
2 整桶私有,二维码也走公开代理 二维码多一跳,无必要
3 管理端短时预签名 与「代理 + 审计」决策不符

4. 存储与权限

4.1 MinIO bucket policy

  • 允许匿名 s3:GetObject,Resource 限定:arn:aws:s3:::ajyy/qrcode/*
  • 撤销整桶 ajyy/* 匿名可读策略
  • 后端启动 _ensure_bucket 时写入上述前缀策略(幂等覆盖)

4.2 上传 folder 约束

场景 folder 接口
二维码图片 固定 qrcode 仅管理端 POST /api/web/admin/attachment/upload
在线反馈附件 固定 feedback 公开 POST /api/public/attachment/upload
建言献策附件 固定 suggestion 公开上传同上
  • 公开上传:白名单仅 feedback、suggestion;拒绝其它 folder(含 qrcode)
  • 管理端二维码上传:使用 qrcode;管理端若上传其它业务附件,不得依赖匿名读

4.3 数据存储

  • 业务表 attachments JSON / image_url 仍存服务端返回的 url(完整 MinIO URL 或等价 object 地址)
  • 私有对象匿名 GET 返回 403 即可;无需改写历史 URL 字符串

4.4 上传防可执行 / 防木马(内容安全)

服务端把附件当静态对象存储,永不在服务端执行上传文件(不落地为可执行权限、不调用、不解压执行)。

在现有 upload_validate(扩展名白名单 + 20MB)基础上本期加固:

措施 要求
扩展名白名单 维持并明确禁止可执行与脚本类:如 .exe .dll .bat .cmd .ps1 .msi .scr .js .html .htm .svg .sh 等(不在白名单即拒)
魔数校验 对图片等可校验类型:文件头 magic 须与扩展名一致(如 PNG/JPEG/GIF/WEBP/PDF);不一致则 400,防止「改后缀的 exe」
存储权限(local) 写入文件权限不含执行位(如 0o644)
代理下载响应头 X-Content-Type-Options: nosniff;Content-Type 仅按白名单安全类型推断;非图片默认 Content-Disposition: attachment(避免浏览器当页面执行);禁止下发 application/x-msdownload 等可执行 MIME
二维码上传 仅允许图片扩展名(管理端 qrcode folder)

本期不做: 杀毒引擎扫描、沙箱动态检测、zip 内嵌木马深度扫描(.zip 仍允许作为证据压缩包;管理员本机打开需自行谨慎——写入验收说明即可)。

5. 管理端鉴权代理

5.1 接口

GET /api/web/admin/attachment/file?url=<库内存储的附件URL>
  • 鉴权: JWT;与反馈/建言详情接口一致,使用 get_current_user(任一已登录后台用户)。页面入口仍由菜单 feedback / suggestion 控制;本期不额外叠加 require_menu,避免比详情接口更严导致「能看详情却打不开附件」
  • 校验: 从 url 解析 objectKey,必须属于当前 MINIO_BUCKET(或 local /uploads/ 前缀);拒绝外链、空参数、路径穿越。二维码图由前端直链访问,不依赖本代理
  • 响应: StreamingResponse;安全 Content-Type + nosniff;图片可 inline,其它类型优先 attachment
  • 审计: 应用日志记录操作者、时间、objectKey(本期不强制新审计表)

Local 存储模式:代理从 LOCAL_STORAGE_DIR 读文件,同样需登录。

5.2 web-admin UI

  • 反馈详情: 附件不再用裸 MinIO <a href>;点击后用带 Authorization 的请求取 blob,再 URL.createObjectURL 打开/预览
  • 建言详情: 补齐附件列表展示,走同一打开逻辑
  • 二维码配置: 继续直链 imageUrl(qrcode/* 公开),不走代理

6. 移动端本地预览

6.1 预览

  • 选文件后生成本地预览:
    • H5:URL.createObjectURL(file)
    • 小程序/App:tempFilePath
  • 缩略图 / 文件展示只用 previewUrl,禁止用远程 MinIO url 作为 <image :src>
  • 移除单项或页面卸载时 H5 调用 revokeObjectURL

6.2 上传与提交

  • 仍调用公开上传接口;返回 { id, url, fileName, size }
  • 列表项结构:{ previewUrl, url, fileName, size }
    • UI → previewUrl
    • 提交 attachmentUrls → url(服务端地址)
  • 上传失败不入列表;本地预览不可用时仅显示文件名芯片,不回退远程 URL
  • 提交成功后离开页面;不提供「再次从服务器打开自己刚传附件」

涉及:mobile-wx/utils/upload.js、pages/feedback/index.vue、pages/suggest/index.vue

7. 错误与验收

场景 期望
匿名 GET …/feedback/… 或 …/suggestion/… 403
匿名 GET …/qrcode/… 200
未登录调管理端代理 401
代理非法 url / 外链 400
公开上传 .exe / 假图片(错误魔数) 400
移动端上传页 本地预览正常;远程私有 URL 匿名不可读
管理端详情 登录后可打开反馈/建言附件
二维码配置预览 匿名直链仍可显示

测试:

  • 单测:bucket 前缀策略;代理鉴权与 objectKey 校验;扩展名拒绝可执行类型;魔数与扩展名不一致拒绝
  • 手工:上述验收表

8. 非目标(本期不做)

  • 双 bucket 拆分
  • 管理端预签名下载
  • 公开附件访问审计表 / 下载水印
  • 改写历史附件 URL 字段
  • 移动端提交后再次拉取自己的附件
  • 杀毒引擎 / 沙箱动态分析 / zip 深度扫毒
  • 「仅官方 App 可调公开上传」的客户端绑定(技术上不可靠)

9. 主要改动面

层 文件/模块(预期)
backend app/core/storage.py(前缀策略、读对象);app/core/upload_validate.py(魔数);app/api/admin.py(代理接口);app/api/public.py(folder 白名单);相关 tests
web-admin views/feedback/index.vue;views/suggestion/index.vue;api/admin.ts(代理打开辅助)
mobile-wx utils/upload.js;反馈/建言页预览字段
运维 对已部署环境立即覆盖 bucket policy(启动时或一次性脚本)

10. 风险与回滚

  • 风险: 策略收紧后,若某处仍用私有对象直链,图片/链接会 403——按本设计改 UI 后消除
  • 风险: 允许 .zip 时,压缩包内可能含恶意文件——管理员本机解压需谨慎;本期不深度扫描
  • 回滚: 恢复整桶公开策略可临时救急,但会重新暴露隐私,仅应急使用