Move PRD, design specs, architecture docs, and TDD from .autoflow/ (personal/per-branch layer) to docs/autoflow/ (shared layer that goes into git) per the new Autoflow workspace layout. Files moved: - 02-prd/PRD.md - 03-design/design-review.md - 03-design/user-flow-cross-system.md - 04-architecture/TDD.md - 04-architecture/design-doc.md - 04-architecture/security.md The originals were never tracked, so git mv reduced to a filesystem rename with no history to preserve. .autoflow/ remains for personal notes (progress.md, review reports, testing logs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
15 KiB
Security Notes — Phase 1
本文件記錄 Phase 1 已知的安全設計決策、被接受的風險、以及對應的 mitigation 與 Phase 2 改進候補方案。
更新時機:每次安全審查(Reviewer / Security Auditor)發現新風險或變更現有 trust assumption 時,必須更新此檔案。
索引
| Section | 內容 |
|---|---|
| Trust Boundary | user_id 來源信任問題(Phase 1 接受風險) |
| Input Validation | 已落實的輸入驗證機制 |
| Storage Security | MinIO object key 控制與 cleanup 策略 |
| Auth Security | JWT / JWKS 配置、algorithm pin |
| Rate Limiting | 雙層 rate limiter 設計 |
| Logging | 結構化 log 與敏感資料保護 |
| Phase 2 候補方案清單 | 已知待補強的設計 |
Trust Boundary(重要 design risk)
user_id 為 multipart field,受信任 visionA-backend 帶來
設計
POST /api/v1/jobs 的 user_id 從 multipart form field 傳入,不是從 JWT claim derive。Converter 完全信任 visionA-backend 端把對的 user_id 傳進來。
visionA-backend Converter
│ │
├── client_credentials ──────→│ (取得 access token)
│ │
├── POST /api/v1/jobs ────────→│ Form-Data:
│ Authorization: Bearer … │ user_id: "alice" ← visionA 端決定
│ │ model: <file>
│ │ ...
│ │
│ │ Converter 端:
│ │ - 用 token 驗 client(OK)
│ │ - 信任 user_id 是「真正提交的 user」
│ │ - 不再驗證 user_id 與 token 的關係
Trust assumption(Phase 1)
visionA-backend 端:
- 程式碼安全 — 無 XSS / SSRF / RCE 漏洞,user_id 來源可信
- infra 安全 — network ACL、IP allow-list、TLS 確保只有 visionA-backend 能呼叫此 API
- credential 管理 —
client_secret不外洩、不放 git、不寫 log - audit log 健全 — visionA 端能追溯「哪個真實用戶觸發了哪次轉檔」
Risk(被接受)
visionA-backend 一旦被 compromise,attacker 可用同一個合法 client_credentials:
| 攻擊面 | 影響 |
|---|---|
為任意 user_id 建 job |
冒充任何 user(user_id 完全由 attacker 控制) |
| 鎖定特定 user 7 天 | active_job conflict 機制被當武器(任意 user_id 一旦被鎖,正常請求也 409) |
| 偽造的 job 計入 victim user_id 的 history | user:{victim}:jobs Set 被汙染,未來查 history 看到不是自己的紀錄 |
| 累計 victim 的 job count(如有 quota / billing) | Phase 2 若引入 per-user quota / billing,會誤計到 victim 上 |
Phase 1 決策(2026-04-25 使用者裁決)
接受此風險。 理由:
- visionA-backend 是內部受控系統(非 Internet-facing),compromise 機率低
- Phase 1 重點是把核心 pipeline 跑通,安全強化排在 Phase 2
- 引入 HMAC / OBO 會增加 visionA 端的整合工作量,目前未取得對方確認
Mitigations(Phase 1 已採用)
| Mitigation | 說明 |
|---|---|
| per-client_id rate limiter(300 req / 5min) | 限制單一 client(即被 compromise 的 visionA-backend)的攻擊速度 |
| input 完全由 server 控制 object_key | jobs/{server-生成 uuidv4}/input/{sanitize 後 filename},attacker 無法控制 prefix |
| filename / user_id 嚴格 sanitize | 阻擋 path traversal / Redis key injection / log injection / XSS / glob pattern |
| structured audit log(含 client_id + user_id pair) | 可從 log 反查「哪個 client 為哪個 user_id 建了 job」,發現 compromise 時加速 forensics |
| active_job 7 天 TTL(fail-safe) | 即便 worker 異常未清,TTL 也會自動 GC,避免 attacker 鎖死後永久不釋放 |
Phase 2 候補方案
方案 1:HMAC-signed user_id(推薦短期)
visionA-backend 用共享 secret HMAC 簽 user_id,Converter 驗簽:
visionA-backend Converter
hmac = HMAC-SHA256( 收到 multipart 後:
secret, recv_user_id, recv_hmac
user_id || timestamp) ↓
if HMAC-SHA256(secret, recv_user_id || ts) != recv_hmac:
POST /api/v1/jobs ─────────→ return 401 invalid_hmac
Form: if abs(now - ts) > 60s:
user_id: "alice" return 401 hmac_expired
x_user_id_hmac: "<hex>" else:
x_user_id_ts: "<unix>" accept user_id
優點:實作簡單,雙方只需要共享 secret + 規範 hash。 缺點:仍是 symmetric secret,有外洩風險;不會解決「visionA 自己被 compromise」的場景(attacker 也能簽)。
方案 2:OBO Token / Token Exchange(業界標準,推薦中期)
visionA-backend 為每個 user 取 user-context token(例如 OBO flow / Token Exchange RFC 8693),Converter 從 JWT claims 取 user_id:
visionA-backend Member Center Converter
POST /token ──────────────→ grant_type=token-exchange
subject_token=<user-token> audience=converter
subject_token_type=jwt ↓
new token with claims:
sub: "alice" ← user 身份
actor: { sub: "visionA-client" } ← 委託 client
←─── new access token ─────
POST /api/v1/jobs ─────────────────────────────────────────────────→
Authorization: Bearer <new token> ↓
Converter 從 token claims
取 user_id(不是 multipart)
優點:
- 完全消除 trust boundary 問題(user_id 來自 Member Center 簽過的 JWT)
- 業界標準,跨 vendor 適用
- 自動 audit chain(actor + subject 雙重身份)
缺點:
- visionA / Member Center 都需要實作 Token Exchange 流程
- 性能:每次 POST /jobs 多一次 Token Exchange round-trip(可 cache 緩解)
方案 3:Audit Anomaly Detection(補強)
偵測同 client_id 短期內出現大量不同 user_id 的異常 pattern:
監控 metric:
unique_user_ids_per_client_per_5min{client_id="visionA-backend-client"}
正常 pattern:
- 一個 client_id 5 分鐘內可能服務 5-50 個不同 user_id
異常 pattern:
- 一個 client_id 5 分鐘內出現 500 個不同 user_id
- 一個 client_id 連續 1 小時內每 5 分鐘出現 < 1 秒的 burst(自動化攻擊)
告警 → 人工介入 → 視情況 revoke client_credentials
優點:不需要改動 protocol,可獨立實作;對已 deployed 系統最容易加上。 缺點:被動防禦(事後發現),無法即時阻擋。
Input Validation
已落實(Phase 1)
| 項目 | 實作位置 | 機制 |
|---|---|---|
| filename sanitize | src/utils/sanitize.js sanitizeFilename |
NUL byte truncation / path.posix.basename / 控制字元 / 白名單字元 / 截長 200 / leading-dot 移除 |
| user_id 嚴格白名單 | src/utils/sanitize.js validateUserId |
^[A-Za-z0-9._-]+$ regex(Sec M1 強化)+ 額外 .. 拒絕 |
| version 嚴格白名單 | src/routes/v1/validators/createJob.js |
^[A-Za-z0-9._-]+$ regex(Sec M3) |
| model_id 數字範圍 | 同上 | ^\d+$ + 1 ≤ x ≤ 65535 |
| platform enum | 同上 | {520, 720, 530, 630, 730} |
| enable_ boolean* | 同上 | 嚴格 'true' / 'false' 字串 |
| metadata JSON object | 同上 | JSON.parse + 拒絕 array / null / primitive |
| model 副檔名白名單 | 同上 | {.onnx, .tflite}(PRD F-01) |
| model file 大小 | multer + handler | 預設 500MB(multer LIMIT_FILE_SIZE → 413) |
| ref_image per-file 大小 | validateCreateJobRequest |
10MB(Sec C2 修正,避免 100 張 × 500MB = 50GB OOM) |
| ref_image 張數 | multer fields config | maxCount=100 |
攻擊向量驗證清單
| 攻擊向量 | 防禦點 | 測試 |
|---|---|---|
| Path traversal in filename | sanitizeFilename path.posix.basename + leading-dot strip |
sanitize.test.js |
| NUL byte truncation | sanitizeFilename split('\0') |
同上 |
| Windows path / backslash | sanitizeFilename replace(/\\/g, '/') |
同上 |
| Redis key injection in user_id | validateUserId 拒絕 : |
同上 |
| XSS in user_id | validateUserId 嚴格白名單 |
同上(Sec M1) |
| XSS in version | version 嚴格白名單 |
createJob.validator.test.js(Sec M3) |
| Unicode RTL override | 嚴格白名單拒絕非 ASCII | 同上 |
| Glob / shell metachar in user_id / version | 嚴格白名單拒絕 `*?[];& | $` 等 |
| ref_image OOM (100 × 500MB) | per-file 10MB 上限 | createJob.integration.test.js(Sec C2) |
| log injection (CRLF) | 嚴格白名單拒絕 \r\n |
同上 |
Storage Security
MinIO object key 完全 server 控制
inputObjectKey = `jobs/${jobId}/input/${safeFilename}`
^uuidv4 ^server-controlled prefix ^sanitized
refImageKey = `jobs/${jobId}/ref_images/${index}_${safeFilename}`
attacker 無法控制:
- prefix
jobs/— server hardcode - jobId — server 用
uuidv4()生成 - ref_images index — server 用
idx自增
attacker 部分控制(已 sanitize):
- safeFilename — 經
sanitizeFilename處理(最壞情況產生合法的相對檔名)
M5 方案 A:先寫 MinIO 後 Lua claim
避免「拿到 Lua claim 但 MinIO 失敗」需要 rollback Redis 的複雜度:
- MinIO 失敗 → 直接回 502,Redis 完全乾淨
- Lua conflict / throw → cleanup MinIO(fire-and-forget,靠 7d lifecycle 兜底)
- enqueue 失敗 → 補償 release Redis + cleanup MinIO(Sec M2 + Reviewer Major-2 修正)
Sec M4:寫入放大 pre-check
handler 在 writeInputToMinIO 之前先廉價 GET user:{userId}:active_job,若已存在直接回 409。
避免 conflict request 還是上傳完 500MB 才被 Lua reject(節省頻寬與記憶體)。
⚠️ pre-check 與 Lua claim 之間仍有 race(兩個 request 同時通過 pre-check),最終 atomicity 仍由 Lua 保證;pre-check 純粹是「optimization」。
Sec M5:mount-time STORAGE_BACKEND 檢查
createJobsRouter 在 mount 時就檢查 STORAGE_BACKEND === 'minio':
- 不對 → 不掛 multer,POST /api/v1/jobs 直接回 500 misconfiguration
- 不會吃 multipart body,避免 misconfig 也消耗 500MB 記憶體
- GET / DELETE / download-tokens 不依賴 storage backend,仍正常掛
Auth Security
JWT Algorithm Pin(Sec m3)
src/auth/jwks.js 明確 pin 接受的 JWT signing algorithm:
const ALLOWED_JWT_ALGS = ['RS256', 'ES256', 'PS256'];
拒絕:
none(jose 預設拒絕,但仍明確列出)HS256/HS384/HS512(HMAC,避免演算法混淆攻擊)
JWKS Cache
- TTL 10 分鐘(
JWKS_CACHE_MAX_AGE_MSenv override) - Cooldown 30 秒(避免 JWKS endpoint 失敗時 thundering herd)
- 模組層級 cache(同一個 jwksUrl 共用一個 RemoteJWKSet)
Token 驗證
| 檢查項 | jose 預設 | Converter 加碼 |
|---|---|---|
| signature | ✅ | — |
| exp | ✅ | clockTolerance 60s |
| nbf | ✅ | — |
| issuer | — | ✅(MEMBER_CENTER_ISSUER) |
| audience | — | ✅(KNERON_CONVERTER_AUDIENCE) |
| algorithm | 拒絕 none | ✅ pin to RS256/ES256/PS256(Sec m3) |
Rate Limiting
雙層設計
Request → IP-based limiter (200 req / 15min) ← app.js 全域
→ requireAuth (驗 token)
→ per-client_id limiter (300 req / 5min) ← v1 jobs router
→ multer / handler
| 層級 | 目的 | 範圍 |
|---|---|---|
| IP-based | 防匿名流量 / DDoS | 全 /api 前綴 |
| per-client_id | 合約上限 / 攻擊速度限制 | /api/v1/jobs 寫入端點 |
Phase 2 待補
- Memory store 警告:目前用 process-local memory,多 instance 部署需改 Redis store
- 目前對「未認證但合法路由」的 quota 計算可能誤殺 — 預設 1 個 visionA-backend IP 帶多 user 共用,需要監控 IP-based 是否誤殺
Logging
結構化 JSON
所有 log 使用結構化 JSON 格式,必含:
timestamp(ISO 8601)level(INFO / WARN / ERROR)service(task-scheduler)request_id(貫穿請求生命週期)action(domain.action格式)
敏感資料保護
絕對不寫 log(已逐條檢查):
- token / Authorization header
- file body / model 內容
- MinIO secret / OAuth client_secret
- JWT payload 完整 dump(只記
client_id/tenant_id/user_id)
遮罩處理(如有需要在 Phase 2 加):
- 原始 filename(已 sanitize)— 通常不視為敏感
- IP(log 仍記,但 GDPR 場景可能需要遮罩)
Sec C1 暫緩
.env 一度被 commit 進 git history(健檢時發現),已加入 .gitignore 但 history 仍可追溯。
決策:
- Phase 1 暫緩(2026-04-25 使用者裁決)
- Phase 1 ready 後會做一次 git history rewrite + 強制 rotate 所有 secret
- 在那之前,所有 secret 都被視為「可能已外洩」,dev 環境用 dummy secret,prod 用全新生成的 secret
Phase 2 候補方案清單
已知待補強(依優先級)
| # | 項目 | 優先級 | 預期任務 |
|---|---|---|---|
| 1 | HMAC-signed user_id 或 OBO token(解決 Trust Boundary) | HIGH | Phase 2 — auth 強化 |
| 2 | Git history rewrite(清掉 .env 洩漏) | HIGH | Phase 1 ready 收尾 |
| 3 | MULTIPART_MODEL_MAX_BYTES env 串接(目前寫死 500MB) | MEDIUM | T10 |
| 4 | MAX_CONCURRENT_UPLOADS semaphore(防多 user 並發 OOM) | MEDIUM | T10 |
| 5 | Stream storage 評估(取代 memoryStorage,根本解決 OOM) | MEDIUM | Phase 2 — infra |
| 6 | Rate limiter Redis store(多 instance 部署前提) | MEDIUM | Phase 2 — infra |
| 7 | Audit anomaly detection(user_id pattern 異常告警) | LOW | Phase 2 — observability |
| 8 | Filename Unicode normalization(極端 unicode bypass) | LOW | Phase 2 — security 細修 |
| 9 | Metadata prototype pollution 防護(白名單 keys) | LOW | Phase 2 — security 細修 |
| 10 | Token revocation list / JWT blacklist(無此需求現在) | LOW | Phase 2 — auth |
變更歷史
| 日期 | 變更 | 觸發 |
|---|---|---|
| 2026-04-25 | 初版 | T5 Reviewer + Security Audit 修復 |