Files
xiaoxia-saas/docs/全面代码审计报告-2026-06-21.md
T

17 KiB
Raw Blame History

全面代码审计报告 - 2026-06-21

一、审计目标

对小虾 SaaS 进行一次系统性代码审计,重点不是局部补丁,而是发现并修复:

  • 架构分层不一致
  • 重复入口和重复实现
  • 安全与认证缺陷
  • 外部依赖导入副作用
  • 配置真源缺失
  • 可验证的 P0/P1 Bug

二、本轮已确认并修复的问题

1. P0:简化认证使用 SHA256 存储密码

涉及文件apps/api/app/api/routes/auth_simple.py

问题

  • 注册使用 hashlib.sha256(password) 保存密码。
  • 登录用同样 SHA256 比对。
  • 这是不可接受的密码存储方案,缺少 salt 和 cost factor。

根因

  • auth_simple.py 作为临时实现进入真实路由后没有回收。
  • 项目已有 PasswordHasher(bcrypt),但真实 API 没有复用。

修复

  • 注册改为 password_hasher.hash_password()
  • 登录改为 bcrypt 校验。
  • 对历史 SHA256 用户做透明迁移:首次成功登录后自动升级为 bcrypt。
  • 增加密码强度校验。

2. P0:登录返回不可验证随机 token

涉及文件apps/api/app/api/routes/auth_simple.pyapps/api/app/config.py

问题

  • 登录返回 secrets.token_urlsafe(32)
  • Token 没有签名、没有 payload、没有过期时间、无法被标准认证中间件验证。

根因

  • 临时认证实现绕过了项目已有 JWT 设计。
  • API settings 缺少 JWT_SECRET_KEY 配置项。

修复

  • 登录返回 HS256 JWT。
  • JWT payload 包含 sub/email/type/iat/exp
  • 响应新增 expires_in
  • Settings 增加 JWT_SECRET_KEY,由 .env 可覆盖。

3. P1:API 路由包存在重复入口和导入副作用

涉及文件apps/api/app/api/routes/__init__.py

问题

  • routes/__init__.py 维护了一份重复 api_router
  • 正式入口 app/api/router.py 也维护一份 api_router
  • 两者内容不一致,旧入口缺少后续生成/成片路由。
  • 导入 app.api.routes 会连带导入所有路由和外部依赖。

根因

  • 迁移过程中保留了旧入口,没有明确唯一真源。

修复

  • routes/__init__.py 改为兼容转发,只导出 canonical app.api.router 中的 api_routerhealth_router

4. P1OSS SDK 为硬导入,导致无 OSS 本地环境无法导入 API Router

涉及文件apps/api/app/core/storage.py

问题

  • import oss2 在模块顶层执行。
  • 本地/测试环境未安装 oss2 时,导入任何包含 generated_videos 的 API router 都会失败。

根因

  • 外部存储适配器没有做到依赖可选和配置驱动。
  • 无 OSS 配置时,本地 generated 文件已经可以工作,但代码仍强制要求 OSS SDK。

修复

  • oss2 改为可选导入。
  • 只有配置了 OSS AK/SK 并实际创建 bucket 时才要求 oss2
  • 未配置 OSS 时,下载 URL 对本地 /generated-files/ 直接返回,对 OSS URL 回退为公开 URL。
  • 上传/下载等 OSS 专属操作在未配置时返回明确错误。

5. P1API 配置存在双真源

涉及文件apps/api/app/config.pyapps/api/app/core/config.py

问题

  • app/config.pyapp/core/config.py 同时定义 Settings。
  • 两套配置字段命名、OSS/MinIO、JWT、CORS 等含义不一致。
  • 新代码主要使用 app.config,旧模块可能误导入 app.core.config

根因

  • 早期目录重构后没有清理旧配置入口。

修复

  • 明确 app.config 为 API 配置唯一真源。
  • app/core/config.py 改为兼容转发层,只 re-export canonical Settings/get_settings/settings。
  • 避免未来继续在旧文件增加新字段导致漂移。

6. P1:旧认证/工作空间路由引用已移除的 DI Container

涉及文件apps/api/app/api/routes/auth.pyapps/api/app/api/routes/workspaces.pyapps/api/app/middleware/auth.py

问题

  • 旧完整认证和 workspace 路由引用 get_container()
  • apps/api/app/dependencies.py 已无 get_container()
  • 当前 canonical router 未挂载它们,但任何误导入都会成为运行时炸弹。

根因

  • DI 重构后旧路由未删除也未迁移。
  • “完整实现”文件名误导后续开发者可能重新挂载坏代码。

修复

  • 旧模块顶部显式标记为 legacy disabled。
  • 误导入时立即抛出清晰错误,而不是深层 ImportError。
  • 后续只能在重建 DI composition root 后重新启用。

7. P1:CI 质量门禁只打印版本,不执行检查

涉及文件.gitea/workflows/ci-cd.yml.github/workflows/ci-cd.yml

问题

  • CI 安装/验证工具版本后直接成功。
  • black/isort/flake8/bandit/pytest 没有实际检查代码。
  • develop/main 可能在红线问题未暴露时继续部署。

根因

  • 早期为了验证 CI 环境可用,留下了占位命令。
  • 后续没有升级为真实质量门禁。

修复

  • CI 改为执行 compileallblack --checkisort --check-onlyflake8bandit
  • 测试步骤改为 python -m pytest tests -q
  • Gitea 与 GitHub workflow 保持一致,避免 CI 双轨漂移。

8. P1Domain 层直接依赖 Redis/SMTP

涉及文件packages/domain/auth/session_store.pypackages/domain/auth/email_service.pypackages/adapters/redis/session_store.pypackages/adapters/smtp/email_service.py

问题

  • Domain 层直接 import Redis、SMTP、email MIME 等基础设施实现。
  • packages.domain.auth.__init__ 导入时创建 Redis/SMTP 全局单例。
  • Application use case 通过模块全局变量调用外部服务,测试只能 patch 全局变量。

根因

  • 基础设施实现被放进了 domain 包,破坏 Clean Architecture 依赖方向。
  • 外部服务没有通过构造注入进入 use case。

修复

  • Redis session 实现迁移到 packages/adapters/redis/session_store.py
  • SMTP email 实现迁移到 packages/adapters/smtp/email_service.py
  • Domain 原路径保留兼容 shim,但不再导出全局外部服务单例。
  • 注册、登录、登出、密码重置、邀请成员 use case 改为构造注入,默认使用 adapter 懒加载工厂。
  • 相关测试从 patch 模块全局变量改为注入 mock 服务。

9. P1API 路由层直接依赖具体 SQLAlchemy Adapter

涉及文件apps/api/app/api/routes/*.pytests/unit/test_architecture_boundaries.py

问题

  • 多个 API route 直接 import SQLAlchemy*Repository
  • 路由层本应只依赖 FastAPI dependency provider 和 application use case,不应知道具体仓储实现。
  • 具体 adapter 只能出现在 composition root,例如 apps/api/app/dependencies.py

根因

  • 为了类型标注方便,把基础设施类型泄漏进 API route。
  • 没有架构守卫测试阻止回归。

修复

  • 移除 route 层对 packages.adapters.sqlalchemy_impl 的直接 import。
  • route 层仓储参数改为 Any,具体实现保留在 dependencies.py 注入。
  • 新增 tests/unit/test_architecture_boundaries.py,防止 route 层再次直接 import SQLAlchemy adapter。
  • 同时守住 domain.auth 不再导出 Redis/SMTP 全局单例。

10. P1Postgres psycopg Adapter 与 SQLAlchemy Adapter 双轨

涉及文件packages/adapters/postgres/*tests/unit/test_architecture_boundaries.py

问题

  • packages/adapters/postgres/*packages/adapters/sqlalchemy_impl/* 同时存在。
  • 主运行链路实际使用 SQLAlchemy,但旧 postgres package 仍可被新代码误导入。
  • 双仓储会造成连接池、事务、模型字段、schema 迁移多头漂移。

根因

  • 历史 psycopg 实现被 SQLAlchemy 替代后没有明确废弃边界。
  • 缺少测试禁止运行时代码继续引用旧 adapter。

修复

  • 删除 packages/adapters/postgres/* 旧 psycopg adapter 文件。
  • 新增/强化架构守卫:apps/packages/tests/ 不得 import packages.adapters.postgresPostgres*
  • SQLAlchemy 明确为 SaaS 当前唯一主线持久化 adapter。

11. P1:部署入口双轨与根 Docker 文件漂移

涉及文件Dockerfiledocker-compose.ymlinfra/docker/*docs/DEPLOYMENT.md

问题

  • Dockerfileinfra/docker/api.Dockerfile 启动路径不同。
  • docker-compose.ymlinfra/docker/compose.yml 拓扑不同。
  • 实际 staging/CI 使用 infra/docker,但根入口仍可能被误用。

根因

  • 早期单 API 部署文件没有在多服务架构形成后废弃。
  • 部署文档仍保留旧命令,容易引导错误操作。

修复

  • Dockerfile 改为 deprecated sentinel,误 build 会失败并提示使用 infra/docker/api.Dockerfile
  • docker-compose.yml 改为 deprecated sentinel,误 up 会失败并提示使用 infra/docker/compose.yml
  • docs/DEPLOYMENT.md 改为声明唯一部署入口和 Gitea Actions 部署约定。

12. P1:本地 CI 虚拟环境被误跟踪

涉及文件.gitignore.venv-ci-root/

问题

  • .venv-ci-root 中 3000+ 个第三方依赖文件被 Git 跟踪。
  • 搜索、审计、CI 和 diff 都会被虚拟环境噪音污染。

根因

  • .gitignore 没有忽略 .venv-ci-root/
  • 本地 CI 工具环境被误加入索引。

修复

  • .gitignore 增加 .venv-ci-root/
  • 从 Git 索引移除 .venv-ci-root,本地文件保留。

13. P1:数据库 Schema 多真源漂移

涉及文件init-tables.sqlmigrations/*.sqlpackages/adapters/sqlalchemy_impl/models.pydocs/SCHEMA-MAINLINE.md

问题

  • init-tables.sqlmigrations/*.sql、SQLAlchemy models 曾同时定义表结构。
  • 字段已经漂移,例如历史 SQL 使用 assets.library_id/storage_key/mime_type,当前 SQLAlchemy 使用 assets.asset_library_id/file_url/file_type
  • 旧运行时曾由 SQLAlchemy Base.metadata.create_all() 建表,部署缺少正式 migration 主线。

根因

  • Phase 迁移过程中 SQL 快照没有退役。
  • Alembic 迁移链曾存在但 revision 已过期,和当前 SQLAlchemy model 不一致。

修复

  • init-tables.sql 改为 deprecated sentinel,误执行会直接失败。
  • 新增并更新 docs/SCHEMA-MAINLINE.md,明确 SQLAlchemy models 是 schema 定义真源,Alembic 是迁移执行真源。
  • 删除 stale Alembic revisions,新增 alembic/versions/001_current_schema_baseline.py 作为当前 schema baseline。
  • alembic/env.py 改为从 DATABASE_URL 读取连接,并启用 compare_type=True
  • infra/docker/deploy-staging.sh 在服务启动前执行 alembic upgrade head;已有业务表但无版本表时执行 alembic stamp head && alembic upgrade head
  • API/Worker 默认关闭 AUTO_CREATE_SCHEMA,不再默认调用 Base.metadata.create_all()
  • staging 已验证:alembic_version = 001health check healthydevelop 部署 job succeeded。
  • 架构守卫禁止运行时代码引用 init-tables.sql 或历史 migrations/001_initial_schema.sql

三、已补充测试

新增

  • tests/unit/test_auth_simple.py
  • tests/unit/test_login_use_case.py
  • tests/unit/test_register_user_use_case.py
  • tests/unit/test_password_reset_use_case.py
  • tests/unit/test_architecture_boundaries.py

覆盖:

  • 登录 token 是可验证 JWT。
  • bcrypt 密码可登录。
  • 历史 SHA256 密码登录后自动升级 bcrypt。
  • 错误密码拒绝且不会写库。

已运行通过

python -m pytest tests/unit/test_architecture_boundaries.py tests/unit/test_auth_simple.py tests/unit/test_password_hasher.py tests/integration/test_generation_pipeline.py tests/integration/test_projects.py -q
python -m pytest tests/unit/test_login_use_case.py tests/unit/test_register_user_use_case.py tests/unit/test_password_reset_use_case.py tests/unit/test_invite_member_use_case.py tests/unit/test_session_store.py tests/unit/test_email_service.py -q

结果:34 passed52 passed

四、仍需继续治理的问题

P1:认证体系双轨(已收敛)

当前认证入口已收敛到 canonical route

  • apps/api/app/api/routes/auth.py
  • apps/api/app/auth.py
  • packages/application/auth/*
  • packages/adapters/sqlalchemy_impl/user_repository.py

已完成:

  • auth.py 不再是 disabled skeleton,也不依赖缺失的 get_container()
  • api/router.py 直接挂载 app.api.routes.auth
  • auth_simple.py 兼容 shim 已删除,认证入口只保留 auth.py
  • 登录/注册/当前用户均通过 UserRepository + UseCase + app.authroute 层不再承载认证业务逻辑。

剩余建议:

  1. 持续用 grep/架构测试防止 auth_simple.py 路径回流。
  2. 根据产品需要补齐 password reset / verify email 的正式 route。
  3. 接入真实邮件 adapter 前继续保持 no-op email delivery。

P1:认证扩展路由缺失(已补齐)

已完成:

  • GET /api/v1/auth/verify-email?token=...
  • POST /api/v1/auth/verify-email
  • POST /api/v1/auth/password/forgot
  • POST /api/v1/auth/forgot-password
  • POST /api/v1/auth/password/reset
  • POST /api/v1/auth/reset-password

说明:

  • 同时保留后端历史路径和前端现有路径,避免前后端命名漂移导致功能不可用。
  • 路由仍通过 UserRepository + UseCase,不在 route 层写业务逻辑。
  • 邮件发送在当前 compatibility route 中仍使用 no-op email service,接入真实 SMTP adapter 前不会对外发信。
  • 已修复 password reset token 过期判断中的 naive/aware datetime 比较风险。

P1:外部服务 adapter 化收尾(已配置驱动)

已完成:

  • 认证 route 不再持有私有 no-op email/session 类,改为从 apps/api/app/dependencies.py 获取配置驱动 adapter。
  • ENABLE_EMAIL_DELIVERY=false 时使用 no-op email servicetrue 时按 SMTP 配置创建 EmailService
  • ENABLE_REDIS_SESSIONS=false 时使用 no-op session storetrue 时按 REDIS_URL 创建 Redis session store。
  • SMTP/Redis no-op 实现放在 adapter 层,use case 仍通过构造注入使用。

剩余建议:

  • Production 开启前需要真实 SMTP/Redis 连接验证和密钥审计。

P1:Domain 层存在外部基础设施依赖

涉及:

  • packages/domain/auth/session_store.py 直接依赖 Redis。
  • packages/domain/auth/email_service.py 直接依赖 SMTP。

问题:

  • 违反 Clean Architecture。
  • Domain import 会创建全局外部服务实例。
  • 错误处理使用 print 和吞异常。

建议:

  • 抽象 SessionStoreEmailSender port。
  • Redis/SMTP 实现迁移到 adapters。
  • UseCase 通过构造函数注入 port。

P1Repository 体系双轨(已收敛)

当前持久化主线:

  • packages/adapters/sqlalchemy_impl/*

已完成:

  • packages/adapters/postgres/* psycopg adapter 已删除。
  • 架构守卫禁止运行时代码重新引用 packages.adapters.postgresPostgres*
  • Workspace/User/Member/Invitation 已走 SQLAlchemy adapter 与 API composition root。

剩余建议:

  • 继续检查文档和脚本中的旧术语,避免恢复 psycopg adapter。

P1:存储命名漂移(已收敛)

已完成:

  • 运行时代码改用 OSSStorageService / get_storage_service 命名。
  • apps/api/app/core/storage.py 仅保留 MinIOService / get_minio_service 兼容别名作为边界层。
  • 架构守卫禁止 runtime/test 代码继续引用 MinIO 命名。
  • 当前主线为 OSS;未配置 OSS 时,生成文件通过本地 /generated-files/ fallback 公开访问。

P2Bandit 安全告警(已清零)

已完成:

  • B104:API bind 地址改为配置驱动,容器内 0.0.0.0 明确标注为 bind address,外部暴露由 Docker/Nginx 控制。
  • B104:登录默认 ip_address0.0.0.0 改为 unknown,避免把未知 IP 伪装成 bind-all 地址。
  • B110:worker 失败状态更新不再吞异常;返回 state_error 并执行 rollback。
  • B404/B603/B607FFmpeg/FFprobe 通过固定参数列表、shell=Falseshutil.which 解析路径,并用 nosec 标注剩余可接受风险。
  • 当前 python -m bandit -r apps packages -q 通过。

P2:临时代码仍在主线

发现:

  • auth_simple.py shim 已删除。
  • 部分测试和文档仍引用旧 MinIO/OSS 混合术语。
  • __pycache__ 文件出现在工作树扫描中,需确认 .gitignore 和仓库状态。

P1:生产 Alembic 准备(已形成 checklist

已完成:

  • 新增 docs/PRODUCTION-RELEASE-CHECKLIST.md
  • 明确生产发布前必须执行:staging 验证、CI 验证、生产 env 审计、数据库备份、Alembic 状态判断、发布后 smoke。
  • 明确首次生产接入 Alembic 的判断规则:已有 alembic_version 则 upgrade;有业务表但无版本表则 stamp head 后 upgrade;空库直接 upgrade。
  • 明确回滚策略:应用优先回滚 tag;数据库只在验证过 downgrade 时使用 downgrade,否则以备份恢复作为最终兜底。

剩余建议:

  • 真正生产发布前,基于当时生产容器名和 DB 名写一次性恢复 runbook。

五、下一步建议修复顺序

  1. Production 外部服务上线:验证 SMTP/Redis 连接、密钥和告警。
  2. 验证真实 SMTP/Redis/OSS 凭证并完成生产外部服务 smoke。
  3. 全量测试和 CI:后端 unit/integration + 前端 type-check/build + staging smoke。