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>
348 lines
15 KiB
Markdown
348 lines
15 KiB
Markdown
# Security Notes — Phase 1
|
||
|
||
> 本文件記錄 Phase 1 已知的安全設計決策、被接受的風險、以及對應的 mitigation 與 Phase 2 改進候補方案。
|
||
>
|
||
> **更新時機**:每次安全審查(Reviewer / Security Auditor)發現新風險或變更現有 trust assumption 時,必須更新此檔案。
|
||
|
||
## 索引
|
||
|
||
| Section | 內容 |
|
||
|---------|------|
|
||
| [Trust Boundary](#trust-boundary重要-design-risk) | user_id 來源信任問題(Phase 1 接受風險) |
|
||
| [Input Validation](#input-validation) | 已落實的輸入驗證機制 |
|
||
| [Storage Security](#storage-security) | MinIO object key 控制與 cleanup 策略 |
|
||
| [Auth Security](#auth-security) | JWT / JWKS 配置、algorithm pin |
|
||
| [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 傳入,**不是**從 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 端:
|
||
|
||
1. **程式碼安全** — 無 XSS / SSRF / RCE 漏洞,user_id 來源可信
|
||
2. **infra 安全** — network ACL、IP allow-list、TLS 確保只有 visionA-backend 能呼叫此 API
|
||
3. **credential 管理** — `client_secret` 不外洩、不放 git、不寫 log
|
||
4. **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 使用者裁決)
|
||
|
||
**接受此風險。** 理由:
|
||
1. visionA-backend 是內部受控系統(非 Internet-facing),compromise 機率低
|
||
2. Phase 1 重點是把核心 pipeline 跑通,安全強化排在 Phase 2
|
||
3. 引入 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:
|
||
|
||
```js
|
||
const ALLOWED_JWT_ALGS = ['RS256', 'ES256', 'PS256'];
|
||
```
|
||
|
||
拒絕:
|
||
- `none`(jose 預設拒絕,但仍明確列出)
|
||
- `HS256` / `HS384` / `HS512`(HMAC,避免演算法混淆攻擊)
|
||
|
||
### JWKS Cache
|
||
|
||
- TTL 10 分鐘(`JWKS_CACHE_MAX_AGE_MS` env 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 修復 |
|