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

398 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 全面代码审计报告 - 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.py``apps/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_router``health_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.py``apps/api/app/core/config.py`
**问题**
- `app/config.py``app/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.py``apps/api/app/api/routes/workspaces.py``apps/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 改为执行 `compileall``black --check``isort --check-only``flake8``bandit`
- 测试步骤改为 `python -m pytest tests -q`
- Gitea 与 GitHub workflow 保持一致,避免 CI 双轨漂移。
### 8. P1Domain 层直接依赖 Redis/SMTP
**涉及文件**`packages/domain/auth/session_store.py``packages/domain/auth/email_service.py``packages/adapters/redis/session_store.py``packages/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/*.py``tests/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.postgres``Postgres*`
- SQLAlchemy 明确为 SaaS 当前唯一主线持久化 adapter。
### 11. P1:部署入口双轨与根 Docker 文件漂移
**涉及文件**`Dockerfile``docker-compose.yml``infra/docker/*``docs/DEPLOYMENT.md`
**问题**
-`Dockerfile``infra/docker/api.Dockerfile` 启动路径不同。
-`docker-compose.yml``infra/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.sql``migrations/*.sql``packages/adapters/sqlalchemy_impl/models.py``docs/SCHEMA-MAINLINE.md`
**问题**
- `init-tables.sql``migrations/*.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 = 001`health check healthy`develop` 部署 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。
- 错误密码拒绝且不会写库。
### 已运行通过
```bash
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 passed``52 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.auth`,route 层不再承载认证业务逻辑。
剩余建议:
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 service`true` 时按 SMTP 配置创建 `EmailService`
- `ENABLE_REDIS_SESSIONS=false` 时使用 no-op session store`true` 时按 `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` 和吞异常。
建议:
- 抽象 `SessionStore``EmailSender` port。
- Redis/SMTP 实现迁移到 adapters。
- UseCase 通过构造函数注入 port。
### P1Repository 体系双轨(已收敛)
当前持久化主线:
- `packages/adapters/sqlalchemy_impl/*`
已完成:
-`packages/adapters/postgres/*` psycopg adapter 已删除。
- 架构守卫禁止运行时代码重新引用 `packages.adapters.postgres``Postgres*`
- 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_address``0.0.0.0` 改为 `unknown`,避免把未知 IP 伪装成 bind-all 地址。
- B110:worker 失败状态更新不再吞异常;返回 `state_error` 并执行 rollback。
- B404/B603/B607FFmpeg/FFprobe 通过固定参数列表、`shell=False``shutil.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。