jim800121chen d8a9517c9d feat(task-scheduler): Phase 0.8b — API key auth + /result endpoint
Auth pillar 從 OAuth 2.0 resource server 改成 pre-shared API key
(visionA ↔ converter 1:1 internal trust)。新增 GET /api/v1/jobs/:id/result
streaming endpoint 給 visionA backend 中轉 NEF 下載。

Phase A(auth 切換):
- 新增 apiKeyMiddleware(constant-time compare、tokenFingerprint、4 audit events)
- 砍 OAuth middleware + JWKS(保留 oauthClient 供 promote → FAA 使用)
- 4 個 endpoint 換掛 requireApiKey
- 加 TRUST_PROXY env + Express trust proxy 設定(forensic source_ip)

Phase B(/result endpoint):
- streaming NEF download with 5min timeout + concurrent cap 10
- Two-tier rate limit(burst 5/10s + sustained 20/min)
- Bandwidth quota(1 GB/hr + 6 GB/24hr)by token_fingerprint
- Range header silently ignored + Accept-Ranges: none
- filename quote-escape + RFC 5987 fallback + sanitize
- 8 個 /result audit events(forensic 完整)

設計演進記錄:docs/TODO-visionA-integration-v2.md(5/2 OAuth → 5/16 API key
→ 5/16 download via converter;對應 visionA repo ADR-015/016)

Tests: 597 → 666 (+69)、29 suites all pass
Security: APPROVE WITH CONDITIONS(單 instance 部署、6 新 env、24hr 監控)
npm audit: 3 vuln → 0(transitive AWS SDK xml chain)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:47:28 +08:00

469 lines
25 KiB
Markdown
Raw 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.

# Security Notes — Phase 1 + Phase 0.8b
> 本文件記錄 Phase 1 + 0.8b 已知的安全設計決策、被接受的風險、以及對應的 mitigation 與 Phase 2 改進候補方案。
>
> **更新時機**每次安全審查Reviewer / Security Auditor發現新風險或變更現有 trust assumption 時,必須更新此檔案。
>
> **Phase 0.8b 主要變動**visionA → converter 對外 auth 從 OAuth JWT 改 pre-shared API keyADR-010。Trust boundary 模型不變,但 auth mechanism 大幅簡化。詳見下方「Auth Security」與「API Key Management」章節。
## 索引
| Section | 內容 |
|---------|------|
| [Trust Boundary](#trust-boundary重要-design-risk) | user_id 來源信任問題Phase 1 接受風險Phase 0.8b 風險模型不變) |
| [Input Validation](#input-validation) | 已落實的輸入驗證機制 |
| [Storage Security](#storage-security) | MinIO object key 控制與 cleanup 策略 |
| [Auth Security](#auth-security) | **Phase 0.8b 改寫**API key + (保留) OAuth client for promote |
| [API Key Management](#api-key-management) | **Phase 0.8b 新增**CONVERTER_API_KEY rotation / 部署 / 外洩處理 |
| [Rate Limiting](#rate-limiting) | 雙層 rate limiter 設計 |
| [Logging](#logging) | 結構化 log 與敏感資料保護 |
| [Phase 2 候補方案清單](#phase-2-候補方案清單) | 已知待補強的設計 |
---
## Trust Boundary重要 design risk
### user_id 為 multipart field受信任 visionA-backend 帶來
#### 設計
`POST /api/v1/jobs``user_id` 從 multipart form field 傳入,**不是**從 token claim derive。Converter 完全信任 visionA-backend 端把對的 `user_id` 傳進來。
```
Phase 0.8b 後:
visionA-backend Converter
│ │
├── 帶 CONVERTER_API_KEY ──────│ (pre-shared、雙端對齊)
│ │
├── POST /api/v1/jobs ────────→│ Form-Data:
│ Authorization: │ user_id: "alice" ← visionA 端決定
│ Bearer <CONVERTER_ │ model: <file>
│ API_KEY> │ ...
│ │
│ │ Converter 端:
│ │ - constant-time compare API keyOK
│ │ - 信任 user_id 是「真正提交的 user」
│ │ - 不再驗證 user_id 與 caller 的關係
```
#### Trust assumptionPhase 1 + 0.8b
visionA-backend 端:
1. **程式碼安全** — 無 XSS / SSRF / RCE 漏洞user_id 來源可信
2. **infra 安全** — network ACL、IP allow-list、TLS 確保只有 visionA-backend 能呼叫此 API
3. **credential 管理**`CONVERTER_API_KEY` 不外洩、不放 git、不寫 log**Phase 0.8b 改**:原為 `client_secret`
4. **audit log 健全** — visionA 端能追溯「哪個真實用戶觸發了哪次轉檔」
#### Risk被接受
visionA-backend **一旦被 compromise**attacker 可用同一個合法 `CONVERTER_API_KEY`Phase 0.8b 後Phase 1 為 OAuth `client_credentials`
| 攻擊面 | 影響 |
|-------|------|
| 為任意 `user_id` 建 job | 冒充任何 useruser_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 上 |
| 拿任意 NEFPhase 0.8b 新增 `/result`| API key 模式下 `/result` 沒 per-job authorization、attacker 可拿任意 jobID 的 NEF |
#### Phase 1 / 0.8b 決策2026-04-25 + 2026-05-16 使用者裁決)
**接受此風險。** 理由:
1. visionA-backend 是內部受控系統(非 Internet-facingcompromise 機率低
2. Phase 1 重點是把核心 pipeline 跑通,安全強化排在 Phase 2
3. Phase 0.8b2026-05-16改 API key 後trust model **沒有比 OAuth 更不安全也沒有更安全**
- OAuth 模型client_credentials 一旦外洩attacker 也能冒充 visionA 取 token + 建 job
- API key 模型API key 一旦外洩attacker 直接打 API、相同攻擊面
- 唯一差別OAuth 有 token expiry短週期API key 是長期 secret更需要 rotation 流程)
4. 不引入 HMAC-signed user_id / OBO
- HMAC 仍是 symmetric secret被同樣的 compromise 場景突破
- OBO 需要 MC 實作 token exchange、與 ADR-015 「砍跨團隊依賴」精神相反
- 未來真有 multi-caller 需求再回頭加
#### MitigationsPhase 1 已採用)
| Mitigation | 說明 |
|-----------|------|
| **per-client_id rate limiter300 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 候補方案
##### 方案 1HMAC-signed user_id推薦短期
visionA-backend 用共享 secret HMAC 簽 user_idConverter 驗簽:
```
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 也能簽)。
##### 方案 2OBO Token / Token Exchange業界標準推薦中期
visionA-backend 為每個 user 取 user-context token例如 OBO flow / Token Exchange RFC 8693Converter 從 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 chainactor + subject 雙重身份)
**缺點**
- visionA / Member Center 都需要實作 Token Exchange 流程
- 性能:每次 POST /jobs 多一次 Token Exchange round-trip可 cache 緩解)
##### 方案 3Audit 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._-]+$` regexSec M1 強化)+ 額外 `..` 拒絕 |
| **version 嚴格白名單** | `src/routes/v1/validators/createJob.js` | `^[A-Za-z0-9._-]+$` regexSec 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 | 預設 500MBmulter LIMIT_FILE_SIZE → 413|
| **ref_image per-file 大小** | `validateCreateJobRequest` | 10MBSec 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 失敗 → 直接回 502Redis 完全乾淨
- Lua conflict / throw → cleanup MinIOfire-and-forget靠 7d lifecycle 兜底)
- enqueue 失敗 → 補償 release Redis + cleanup MinIOSec 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 M5mount-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
### Phase 0.8b:分兩條軸
| 軸 | 流向 | Auth mechanism | Phase 0.8b 狀態 |
|----|------|----------------|---------------|
| **對外 API** | visionA → converter | Pre-shared API key`CONVERTER_API_KEY`| **新(取代 OAuth** |
| **Promote** | converter → FAA | OAuth client_credentials既有| **保留** |
### 1. 對外 APIAPI key middlewarePhase 0.8b 新)
#### 設計
- **Header**`Authorization: Bearer <CONVERTER_API_KEY>`(重用 Bearer 格式、client 不需改)
- **比對**`crypto.timingSafeEqual` constant-time compare
- **長度**64 hex chars`openssl rand -hex 32`128 bits 安全強度、遠超 NIST 推薦的 80 bits
- **req.auth 設定**:通過後 `req.auth.clientId = 'visionA-service'`(固定值、給 rate limiter / log 用)
- **失敗行為**401 `invalid_token` + 主動 `socket.destroy()`(沿用 OAuth M2 行為)
- **Fail-fast**env 未設定 → 503 拒絕所有 request不 silently allow
#### 為什麼安全
1. **Constant-time compare**`crypto.timingSafeEqual` 避免 timing attack即使 attacker 用 differential analysis 也無法推斷 key 內容)
2. **長度檢查在 timingSafeEqual 之前**:避免 throw `RangeError`但長度本身公開key 長度為固定值)
3. **64 hex chars** = 256 bits 隨機(`openssl rand -hex 32`brute force 不可行
4. **不 log key 內容**任何方向expected / received 都不 log
#### 失敗情境總表
詳見 `auth.md` §1.4。
### 2. PromoteOAuth client_credentials保留
Converter 仍以自己的身分取 `files:upload.write` token、PUT 結果檔到 FAA。
#### 既有設計(不變)
- **JWT Algorithm Pin**(既有 Sec m3`auth/oauthClient.js` 解析 MC 回應時不需 verify JWT取回後直接帶到 FAAFAA 端負責 verify、與 converter 無關
- **Token cache**per-scopedistance to expiresAt > refreshSkewMs預設 60s算 valid
- **In-flight dedup**:同 scope 並發只發一次 token request
- **AbortController timeout**:預設 10s
- **錯誤分類**`OAuthClientError`4xx不重試/ `OAuthServerError`5xx可重試/ `OAuthTimeoutError`(網路 / timeout可重試
#### 401 處理
FAA PUT 回 401 → `oauthClient.invalidate(scope)` + retry 1 次;仍 401 → 503 `auth_service_unavailable`
### 3. 砍除Phase 0.8b
| 移除 | 原因 |
|------|------|
| `src/auth/middleware.js`OAuth resource server| visionA → converter 不再驗 JWT |
| `src/auth/jwks.js` | 不需要 JWKS cache |
| `MEMBER_CENTER_ISSUER` / `JWKS_URL` env | 不驗 iss / 不取 JWKS |
| `KNERON_CONVERTER_AUDIENCE` env | 不驗 aud |
| `JWKS_CACHE_MAX_AGE_MS` / `JWKS_COOLDOWN_MS` / `JWT_CLOCK_TOLERANCE_SEC` env | 沒有 JWKS / JWT 了 |
| ALLOWED_JWT_ALGS 演算法 pin | 沒有 JWT 驗證了FAA 端有自己的演算法 pin |
### 4. 保留promote 仍需)
| 保留 | 用途 |
|------|------|
| `src/auth/oauthClient.js` | converter → FAA OAuth client |
| `MEMBER_CENTER_TOKEN_URL` env | token endpoint |
| `KNERON_CONVERTER_CLIENT_ID` / `_CLIENT_SECRET` env | converter 作為 OAuth client 的身分 |
| `FILE_ACCESS_AGENT_AUDIENCE` env | FAA 的 audience取 token 時用)|
| `OAUTH_TOKEN_REFRESH_SKEW_MS` / `OAUTH_TOKEN_TIMEOUT_MS` env | token cache 行為 |
---
## API Key Management
### 1. 產生
```bash
openssl rand -hex 32
# 輸出64 個 hex chars
# 範例a3f9b2c1d8e7f6a5b4c3d2e1f0987654321fedcba9876543210abcdef1234567
```
### 2. 部署位置
| 環境 | 位置 |
|------|------|
| dev | `apps/task-scheduler/.env`gitignored |
| stage | docker-compose env / k8s secret |
| prod | docker secret / k8s secret / cloud secrets manager |
### 3. 雙端對齊
- visionA `.env.stage``VISIONA_CONVERTER_API_KEY=<same string>`
- converter `.env``CONVERTER_API_KEY=<same string>`
- **兩端必須完全相同字串**
### 4. 每環境獨立
dev / stage / prod 各自 `openssl rand -hex 32`,絕不共用。
### 5. Rotation 流程
1. 雙端各自準備 stop deployment或允許短暫 401 期)
2. `openssl rand -hex 32` 產新 key
3. 更新雙端 `.env`
4. converter 先 redeploy接受新 key
5. visionA 後 redeploy用新 key call
6. 驗證:`curl -i -H "Authorization: Bearer <NEW_KEY>" https://converter.../api/v1/jobs?user_id=test&limit=1`
**極小停機**做法Phase 0.8b 不實作):暫時讓 converter 接受新舊兩把 keymiddleware 拓展成 array compare、visionA 切到新 key、再砍舊 key。
### 6. 外洩處理
- **立即** rotate 雙端 key
- 檢視 audit log在 rotation 前是否有可疑請求(用 `request_id` + `user_id` 追蹤)
- 若有 anomalous activity同 client_id 短期內 100+ 不同 user_id通報 + forensics
- Post-mortem分析洩漏來源git historyCI logslack並補強
### 7. 絕不做的事
- ❌ **絕不**進 git`.gitignore` 已 exclude `.env`、verify 一次)
- ❌ **絕不**寫進 Slack / email / 對話記錄
- ❌ **絕不**印 logmiddleware 內 log 用 `api_key_length``api_key_set: true` boolean、不印 key 本身)
- ❌ **絕不**在 commit message / PR description 中引用具體值
### 8. Sec C1 暫緩既有風險、Phase 0.8b 仍適用)
`.env` 一度被 commit 進 git history。Phase 0.8b 新加的 `CONVERTER_API_KEY` 必須**極度**注意不要重蹈覆轍。
**Phase 1 ready 後**會做一次 git history rewrite + 強制 rotate 所有 secret含 CONVERTER_API_KEY / 既有的 OAuth client_secret / MinIO secret
---
## 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— 通常不視為敏感
- IPlog 仍記,但 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 secretprod 用全新生成的 secret**
---
## Phase 2 候補方案清單
### 已知待補強(依優先級)
| # | 項目 | 優先級 | 預期任務 |
|---|------|-------|---------|
| 1 | **OBO token / Token Exchange**(解決 Trust Boundary**Phase 0.8b 已決策不做 HMAC 中繼方案**| MEDIUM | Phase 2 — auth 強化前提MC 實作 Token Exchange RFC 8693 |
| 2 | **Git history rewrite**(清掉 .env 洩漏,**含 Phase 0.8b 新加的 CONVERTER_API_KEY**| HIGH | Phase 1 ready 收尾 |
| 3 | **API key automatic rotation**(整合 secrets manager / Vault| MEDIUM | Phase 2 — infra |
| 4 | **API key 並存模式**(讓 middleware 同時接受新舊兩把 key支援極小停機 rotation| LOW | Phase 2 — auth 細修 |
| 5 | **MULTIPART_MODEL_MAX_BYTES env 串接** | DONE | Phase 1 T10 |
| 6 | **MAX_CONCURRENT_UPLOADS semaphore** | DONE | Phase 1 T10 |
| 7 | **Stream storage 評估**(取代 memoryStorage| MEDIUM | Phase 2 — infra |
| 8 | **Rate limiter + bandwidth quota + concurrent stream cap Redis store**(多 instance 部署前提)| **HIGH**2026-05-17 升級Phase B 後 `/result` 開放、attacker blast radius 放大、多 instance 部署前必補)| Phase 2 — infra前提Phase B 後若有 multi-instance 需求) |
| 9 | **Audit anomaly detection**user_id pattern 異常告警)| LOW | Phase 2 — observability |
| 10 | **Filename Unicode normalization**(極端 unicode bypass| LOW | Phase 2 — security 細修 |
| 11 | **Metadata prototype pollution 防護**(白名單 keys| LOW | Phase 2 — security 細修 |
| 12 | **`/result` per-job authorization**API key 模式下 attacker 可拿任意 jobID| **MEDIUM**A.7 follow-up §4 升級)| Phase 2 — auth考慮 client_id 隔離;前提是 #13 完成per-caller credential |
| 13 | **加回 OAuth resource server 並存模式 / 多 caller credential**(多 caller 場景)| **MEDIUM**A.7 follow-up §4 升級)| Phase 2 — 真有第二個 caller 時;#12 的前置工作 |
| 14 | **`/result` 404 vs 410 區分的 jobID enumeration risk** | LOWmarginal、待 #12 完成後可一併處理) | Phase 2 — auth 細修;#12 補後可考慮統一為 404 |
| 15 | **`/result` per-job auth 啟用時、4xx 統一回 404不揭露 lifecycle**2026-05-17 Security review §1 Q5 新增)| LOW → 隨 #12 同步升級為 #12 的子任務 | Phase 2 — auth#12 完成時、attacker 拿到的 key 不再給 full read access、區分 404/410 才變成真正的 leak、屆時應統一回 404 |
| 16 | **MinIO socketTimeout / connectTimeout 對齊 RESULT_STREAM_TIMEOUT_MS**2026-05-17 Security review §1 Q1 / s2 新增)| LOW | Phase 2 — infra確認 MinIO SDK timeout 設定、對齊 5 min stream timeout建議 socketTimeout = STREAM_TIMEOUT - 30s、預留 server-side teardown 時間);避免 MinIO 端 timeout 在 response timeout 前觸發、attacker 看到 stream_error 而非 stream_timeout |
---
## 變更歷史
| 日期 | 變更 | 觸發 |
|------|------|------|
| 2026-04-25 | 初版 | T5 Reviewer + Security Audit 修復 |
| 2026-05-16 | Phase 0.8b 重寫auth 從 OAuth 改 API key新增 API Key Management 章節Trust boundary 風險模型與 Phase 1 一致(只是 secret 形式改變Phase 2 候補清單更新OBO 從 HIGH 降 MEDIUM、新增 API key rotation 相關項) | ADR-010 + ADR-011 |
| 2026-05-17 | Phase B 啟動前 streaming/range design review候補 #12 / #13 升 MEDIUM新增候補 #14404 vs 410 區分的 jobID enumeration trade-off、當前 trust model 下 marginal risk、文件化保留 TODO-v2 §4.1 規格);`/result` 設計補充章節rate limit / Range / audit log / source_filename詳見 `api/api-result.md` §9-§14 | Security Auditor A.7 follow-up §4 + Phase B 啟動前 design review |
| 2026-05-17 | Phase B Security design review 第二輪採納4 Major + 3 Minor候補 #8 從 MEDIUM 升 **HIGH**(多 instance 部署前必補 Redis store、涵蓋 rate limit + bandwidth quota + concurrent cap 三軸);新增候補 #15per-job auth 啟用時 4xx 統一回 404、隨 #12 同步)+ 候補 #16MinIO socketTimeout 對齊 RESULT_STREAM_TIMEOUT_MS`/result` 設計強化rate limit 從 60 req/min single tier 改為 two-tier5 req/10s burst + 20 req/min sustained+ bandwidth quota1 GB/hr + 6 GB/24hr新增 stream response timeout 5 min + concurrent stream cap 10 per-instanceRange header 處理三件事Accept-Ranges: none + silently ignore + log range_attemptedaudit log 從 8 event 擴到 12 event + 強制 A.7 五欄 + /result 特有四欄filename defense-in-depthquote-escape + RFC 5987 + buildFilename assertionBackend acceptance criteria 從 B1-B9 擴充到 AC-1 到 AC-12 + 6 個新 integration test詳見 `api/api-result.md` §9 / §10 / §11 / §13.4a / §15 / §14 | Security Auditor Phase B streaming/range design review §1 Q1-Q6 + §2 修正 + §3 acceptance criteria |