# sub-reviewer (lane 5) — LĂNG KÍNH SECURITY + DATA-RISK — S160 dry-run KHKK→HĐ

**VERDICT: SUA-TRUOC-THI-CONG** — 3 HIGH (F-S1/F-S2/F-S3) phải sửa spec trước khi hmw chạy W6/W8; 6 MED; 5 LOW; 1 PASS-dương.
Đích: `spec-wave-w6-duong-ong-hd-v2-29-07-2026.md` + `spec-wave-w8-authz-29-07-2026.md` + `plan-cha §4`.
Mọi finding = {spec-file:dòng} + {bằng chứng ĐĨA file:line}.

---

## 0. Anchor audit — spec nói vs đĩa (đo 2026-07-29, git HEAD `febe6b1`)

| Anchor spec | Đĩa | Verdict |
|---|---|---|
| `ContractFeatures.cs:334-351` GetEligiblePhases | `:334 internal static List<ContractPhase> GetEligiblePhases(IReadOnlyList<string> userRoles)` → body tới `:351` | ✅ ĐÚNG |
| `ContractFeatures.cs:421-430` PhaseActorRoles | `:421 private static readonly Dictionary<ContractPhase,string[]> PhaseActorRoles` (7 entry, hết ở `:429`) | ✅ ĐÚNG |
| `ContractFeatures.cs:488-489` Forbidden | `:489 throw new ForbiddenException("Bạn không có quyền xem HĐ này.")` | ✅ ĐÚNG |
| `ContractFeatures.cs:298` admin bypass | `:298 if (!currentUser.Roles.Contains(AppRoles.Admin))` | ✅ ĐÚNG |
| `ContractFeatures.cs:113-118` gen mã create-time | `:118 entity.MaHopDong = await codeGenerator.GenerateAsync(...)` | ✅ ĐÚNG |
| `ContractFeatures.cs:632-633` delete guard | `:632 if (entity.Phase >= ContractPhase.DangInKy)` | ✅ ĐÚNG |
| `ContractWorkflowService.cs:49-66` Reject-trước-guard | `:49 if (decision == ApprovalDecision.Reject)` … `:65 return; :66 }` — 0 role-check, 0 phase-check | ✅ ĐÚNG (lỗ THẬT) |
| `ContractWorkflowService.cs:73-78` guard trình | `:73-75 if (!isAdmin && !isSystem && !actorRoles.Contains(Drafter) && !actorRoles.Contains(DeptManager))` | ✅ ĐÚNG |
| `ContractWorkflowService.cs:191-198` admin override | `:191 if (isAdmin) { contract.Phase = targetPhase; … }` | ✅ ĐÚNG |
| `ContractsController.cs:13` class-trần | `:13 [Authorize]` (trần, 0 policy) | ✅ ĐÚNG |
| `ContractsController.cs:28-29` tiền lệ | `:28 [HttpGet("deleted")] :29 [Authorize(Policy="Contracts.Read")]` | ✅ ĐÚNG |
| `ContractsController.cs:38-40` inbox | `:38 [HttpGet("inbox")]` | ✅ ĐÚNG |
| `SuppliersController.cs:67` khuôn Master | `:67 [Authorize(Policy="Suppliers.Update")]` | ✅ ĐÚNG |
| `MenuKeys.cs:41` KeHoachKyKet | `:41 public const string KeHoachKyKet = "KeHoachKyKet";` + `:38-40` comment "CỐ Ý NGOÀI All" | ✅ ĐÚNG |
| `MenuKeys.cs:168` Contracts ∈ All | `:163 All = [` … `:168 Contracts, Forms, Reports,` | ✅ ĐÚNG |
| `AuthorizePolicyRegressionTests` "ĐÃ TỒN TẠI" | `tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs` | ✅ ĐÚNG |
| plan `scripts/backup-sql.ps1` | `scripts/backup-sql.ps1` có mặt | ✅ ĐÚNG |
| W8 "22 endpoint ghi" | đếm tay `ContractsController.cs`: POST 11 (`:46,61,68,85,123,130,137,144,151,158,165`) + PUT 8 (`:53,179,186,193,200,207,214,221`) + DELETE 3 (`:75,110,172`) = **22** | ✅ ĐÚNG (F-S14 dương) |

⇒ **0 anchor sai**. Lỗi của bộ spec này KHÔNG nằm ở anchor — nằm ở **anchor THIẾU** + **ngữ nghĩa seeder** + **hố chưa khai**.

---

## F-S1 [HIGH] W8 §2 bước 3 "seeder grant" — RẤT DỄ THÀNH NO-OP IM LẶNG trên prod ⇒ 403 giết chính dry-run

**Spec:** `spec-wave-w8-authz-29-07-2026.md:19` — "Seeder grant (gotcha #84 — CÙNG COMMIT): cấp `Contracts` Create/Update cho role tham gia dry-run… Grep MỌI site seed-permission (S155 >1 site)".

**Đĩa:** hàng `Permission` cho key `Contracts` **ĐÃ TỒN TẠI SẴN cho mọi role** — `DbInitializer.cs:2156-2170 ContractMenuKeys()` yield `MenuKeys.Contracts` + `Ct_*` vào `reviewKeys` của `SeedAllRolesReviewReadPermissionsAsync` (`:2136`), và nhánh tạo mới ở `:2247-2254` set `CanRead=true, CanCreate=isPe(=false), CanUpdate=false, CanDelete=false`.

🔴 Chỗ chết: `DbInitializer.cs:2231-2245` — với key **non-Pe** (tức `Contracts`), nhánh "row đã có" là:
```
// Key non-Pe: skip-existing (giữ nguyên như cũ).
continue;
```
Nghĩa là: nếu implementer cắm grant Create/Update vào chính `ContractMenuKeys()`/`SeedAllRolesReviewReadPermissionsAsync` (nơi tự nhiên nhất vì key `Contracts` đang sống ở đó), thì **trên prod nó chạy vào `continue` và KHÔNG nâng gì cả** — row `Contracts` đã có từ đợt S159 với `CanCreate=false/CanUpdate=false`. Build xanh, test xanh, log seeder im, deploy xong → **mọi POST/PUT bị 403 im lặng** (đúng gotcha #44 mà spec đang định tránh).

Khuôn ĐÚNG có sẵn trong cùng file: `SeedProcurementMasterAccessAsync` `:2367-2374` (`if (g.R && !row.CanRead) {…} if (g.C && !row.CanCreate) {…}` = **upgrade-if-exists**), chạy SAU revoke để thắng (`:2098-2101` comment).

**Spec không hề nói** row đã tồn tại, không nói phải dùng ngữ nghĩa UPGRADE, không trỏ method-khuôn. "Grep MỌI site" là lời dặn tìm site, không phải lời dặn về **ngữ nghĩa ghi**.

**Acceptance cần thêm (mô tả, không phải code):** spec phải (a) khai rằng row `Contracts` per-role ĐÃ tồn tại với 3 cờ false, (b) pin ngữ nghĩa **upgrade-if-exists** + trỏ khuôn `SeedProcurementMasterAccessAsync:2367-2374`, (c) acceptance đo **CanCreate/CanUpdate** chứ không đo "có row" (đếm row = 0-bit vì row đã có sẵn).

---

## F-S2 [HIGH] Attachment 3 handler = KHÔNG có authz nào ngoài `[Authorize]` trần — W8 CỐ Ý để hở, plan §4 KHÔNG khai

**Spec:** `spec-wave-w8-authz-29-07-2026.md:18` — "GET giữ class-trần (đọc mở như hiện trạng, trừ `/deleted`)". `plan-cha §4` (dòng 51-57) không có mục nào về attachment.

**Đĩa `ContractAttachmentFeatures.cs`:**
- `:56-90 UploadContractAttachmentCommandHandler` — ctor chỉ `(IApplicationDbContext, IFileStorage, IChangelogService)`; **0 `ICurrentUser`, 0 check Drafter, 0 check role, 0 check Phase**. Load HĐ bằng Id rồi ghi thẳng (`:63-64` → `:84-85`).
- `:111-127 DownloadContractAttachmentQueryHandler` — **0 IDOR guard**. Bất kỳ user đăng nhập nào biết `{contractId}/{attId}` → tải được **bản scan ký/đóng dấu** của HĐ bất kỳ. Đây chính là vật GĐ4 (`spec-wave-w7` bản cứng) sẽ upload.
- `:130-152 DeleteContractAttachmentCommandHandler` — **0 IDOR, 0 phase-guard**; xoá được attachment của HĐ `DaPhatHanh` (file storage xoá best-effort `:150-151`).

Đối chiếu: `GetContractQueryHandler` CÓ IDOR guard (`ContractFeatures.cs:482-489`) — tức lá chắn tồn tại ở endpoint HĐ nhưng **không lan sang endpoint con attachment** (`ContractsController.cs:85, :103, :110`).

**Hệ quả cho dry-run:** dữ liệu thật đưa vào ở GĐ4 là **scan hợp đồng đã ký + đóng dấu**. Quyết định "GET giữ class-trần" của W8 giữ nguyên lỗ đọc này. Sau O-B, `DELETE {id}/attachments/{attId}` rơi vào `Contracts.Delete` → grant cho Drafter ⇒ **mọi user role Drafter xoá được scan đã ký của HĐ bất kỳ**.

**Cần:** W8 §① phải liệt attachment như **ô đỏ thứ 3** (hoặc khai tường minh "chấp nhận có ý thức" như O-A), và `plan-cha §4` phải có 1 gạch đầu dòng data-risk cho attachment. Hiện tại là **hố CHƯA KHAI** — đúng câu hỏi (3) của lane.

---

## F-S3 [HIGH] W6 gate 5-anchor THIẾU 2/3 call-site của chính hàm nó bảo sửa ⇒ vá 1 site sót site cùng lớp

**Spec:** `spec-wave-w6:25` gate 5-anchor liệt `ContractFeatures.cs:334-351 · :421-430 · :488-489 · ContractWorkflowService.cs:73-78 · ContractsController.cs:38-40`.

**Đĩa — `GetEligiblePhases` có ĐÚNG 3 call-site:**
- `:301` `ListContractsQueryHandler` (danh sách HĐ) — **KHÔNG trong anchor list**
- `:381` `ListDeletedContractsQuery` (màn "Đã xoá" S159-đợt5) — **KHÔNG trong anchor list**
- `:486` `GetContractQueryHandler` (detail) — có, qua `:488-489`

**Chỗ chết cấu trúc:** chữ ký là `GetEligiblePhases(IReadOnlyList<string> userRoles)` — **chỉ nhận role, không nhận userId, không nhận contract**. Nó **không thể** biểu diễn được điều kiện W6 yêu cầu ("user là `ApproverUserId` của Level trong workflow đã pin **của HĐ đó**"). Cách rẻ nhất mà implementer sẽ làm là *thêm `ChoDuyet` vào tập role* → khi đó `x.c.Phase == ChoDuyet` khớp ở **cả `:301` (list) lẫn `:381` (danh sách đã xoá) lẫn `:486`** ⇒ mở toang toàn bộ HĐ `ChoDuyet` cho mọi user mang role đó.

`spec-wave-w6:17` viết "(hoặc precompute-set, xem 4)" — đặt phương án AN TOÀN làm **lựa chọn ngang hàng trong ngoặc**, không phải bắt buộc. Với đường mòn "sửa hàm 1 chỗ", default sẽ trôi về phương án hở.

**Cần:** (a) anchor bổ sung `:298-302` + `:378-381`; (b) spec CHỐT phương án precompute-set (hoặc overload nhận `contractId`/`userId`) là BẮT BUỘC, cấm mở rộng tập role trả về từ `GetEligiblePhases`; (c) nêu rõ `:381` là màn "Đã xoá" — mở nhầm ở đây = lộ HĐ đã xoá mềm.

---

## F-S4 [HIGH→MED] W6: ca ÂM chống-mở-toang KHÔNG có test tự động, và ca ÂM thủ công 0-bit vì không pin ROLE

**Spec:** `spec-wave-w6:27` liệt 3 test: `Inbox_ApproverCapDangCho_ThayHopDongChoDuyet` · `Inbox_CapKhac_KhongThayHopDong` (ÂM, **inbox**) · `View_ApproverV2_KhongPhaiDrafter_XemDuocHopDongChoDuyet` (**DƯƠNG**, view).
⇒ **0 test ÂM cho VIEW**. Rủi ro lớn nhất (`spec-wave-w6:32` mục C-1 "đổi view-guard làm hở HĐ cho MỌI user") chỉ được canh bằng 1 dòng prod thủ công `:30`.

**`:30` 0-bit:** "user ngoài workflow + không Drafter → `GET /{id}` vẫn 403". Guard là **role-keyed** (`ContractFeatures.cs:486` truyền `currentUser.Roles`). Nếu người kiểm chọn 1 user role `HrAdmin` (chỉ có `DangDongDau` trong `GetEligiblePhases:348`) thì 403 vẫn đúng **kể cả khi** lỗ đã mở toang cho role `CostControl`/`Procurement`. Ca ÂM **không pin role của user thử** ⇒ pass được mà không mang thông tin.

**Cần:** thêm test ÂM cho VIEW (tên cụ thể) + ca ÂM prod phải pin **role của user thử = đúng role bị nghi mở** (hoặc quét 13/13 role). Kèm 1 phép ÂM cho `:381` (list đã xoá).

---

## F-S5 [MED] W8 grant theo ROLE ⟂ W6/V2 duyệt theo USER-ID — 6/13 role ngoài danh sách ⇒ chuỗi duyệt V2 có thể chết giữa đường

**Spec:** `spec-wave-w8:19` grant cho 7 role: Drafter/ProjectManager/DeptManager/Procurement/CostControl/Director/Admin. `spec-wave-w8:18` map `POST {id}/transitions → Contracts.Update`.

**Đĩa:** `ContractWorkflowService.cs:255-266 ApproveV2Async` match approver **thuần theo `ApproverUserId`**, KHÔNG hề đọc role:
```
var allowedUserIds = pendingLevelGroup.Select(l => l.ApproverUserId).ToHashSet();
if (!allowedUserIds.Contains(actorUserId.Value)) throw new ForbiddenException(...)
```
⇒ Designer V2 gán được **user bất kỳ** làm Cấp duyệt. Nếu user đó mang role ∉ 7-role-list (còn lại: Finance, Accounting, Equipment, HrAdmin, AuthorizedSigner, CatalogManager…), họ **403 ngay ở tầng policy** trước khi tới `ApproveV2Async` → HĐ kẹt ở `ChoDuyet`.
Ngoài ra nhánh legacy: 2 transition CUỐI đời HĐ V1 thuộc `AuthorizedSigner` (`DangTrinhKy→DangDongDau`) và `HrAdmin` (`DangDongDau→DaPhatHanh`) — `ContractFeatures.cs:426-427` (`PhaseActorRoles`) — **cả hai đều ngoài grant-list**.

**Giảm nhẹ (đo được):** workflow V2 seed sẵn `QT-HD-V2-001` chỉ có **1 approver = `binh.le@solutions.com.vn` (Lê Văn Bình, CCM)** — `DbInitializer.cs:230-233, :256-262`. CCM ↔ role `CostControl` **CÓ** trong grant-list ⇒ đường mặc định sống. Rủi ro chỉ bật khi dry-run tạo workflow mới qua Designer (W5 pin V2 cho phép chọn).

**Cần:** spec khai luật "grant phải phủ **role của mọi user được gán làm Level** trong workflow được pin", và acceptance thêm 1 bước đo: liệt approver của workflow sẽ dùng → đối chiếu role vs grant-list TRƯỚC deploy.

---

## F-S6 [MED] W8 map "POST → Contracts.Create" nuốt cả comment / attachment / details ⇒ chặn nhầm người có Update mà không có Create; và test tính-chất KHÔNG bắt được map sai

**Spec:** `spec-wave-w8:18` — "POST → `Contracts.Create` · PUT/PATCH → `Contracts.Update` · DELETE → `Contracts.Delete` · `POST {id}/transitions` → `Contracts.Update`. Neo TÍNH-CHẤT: MỌI action HttpPost/Put/Delete có policy — KHÔNG neo con số 22."

**Đĩa:** trong 11 POST có `{id}/comments` (`ContractsController.cs:68`), `{id}/attachments` (`:85`), và 7 `{id}/details/*` (`:123,130,137,144,151,158,165`). Về ngữ nghĩa đây là **sửa HĐ đang có**, không phải tạo HĐ. Map thành `Contracts.Create` ⇒ role được cấp Update-only (ví dụ approver chỉ cần góp ý) **không comment được, không upload được**.

**Chỗ chết của acceptance:** `spec-wave-w8:28` test `MoiEndpointGhi_CoAuthorizePolicy` chỉ khẳng định "có `[Authorize(Policy=...)]`" — **map SAI vẫn PASS**. Test tính-chất đúng hướng (tránh Goodhart số 22) nhưng **mù về nội dung map**.

**Cần:** spec chốt bảng map per-endpoint (ít nhất tách `comments`/`attachments`/`details` sang `Contracts.Update`), và acceptance thêm 1 assert đối chiếu map mong đợi cho ≥3 endpoint đại diện.

---

## F-S7 [MED] `DeleteContractCommandHandler` KHÔNG có check chủ sở hữu — plan §4 chỉ khai chiều "khó xoá", bỏ chiều "ai cũng xoá được"

**Đĩa `ContractFeatures.cs:625-639`:** handler nhận `(IApplicationDbContext db)` — **0 `ICurrentUser`, 0 check `DrafterUserId`, 0 check role**. Chỉ có guard phase `:632`.
**Plan:** `plan-cha:55` (§4 mục 4) chỉ nói "HĐ ≥ phase 5 KHÔNG xóa được qua API" — khai đúng nhưng **chỉ chiều bất tiện rollback**; chiều nguy hiểm (bất kỳ ai có quyền Delete xoá HĐ nháp của **người khác**) không được khai. Hiện trạng: 13/13 role gọi được (class-trần `:13`). Sau O-B: mọi user role Drafter.

**Cần:** khai vào §4 (hoặc W8 §C) + cân nhắc ràng "chỉ Drafter của HĐ hoặc Admin".

---

## F-S8 [MED] W8 acceptance nói verify grant bằng `sqlcmd` RDP — MÂU THUẪN plan cha §7 ("sqlcmd chỉ để local/Dev")

`spec-wave-w8:32`: "grant seeder ăn (đếm Permission rows key `Contracts` per-role qua log seeder/**`sqlcmd` RDP**)".
`plan-cha:84` (§7): "data prod verify **bằng curl API** (SSH-SQL client chết S134/S148; **sqlcmd chỉ để local/Dev**)".
⇒ Acceptance của W8 chỉ định một phương tiện mà plan cha vừa tuyên là không dùng được ở prod. Người thi công sẽ hoặc bỏ qua bước này, hoặc tự chế cách khác ⇒ nghiệm thu trôi.
Ghi chú kỹ thuật: "đếm row" cũng sai trục (xem F-S1) — phải đo **cờ CanCreate/CanUpdate**, và có đường curl hợp lệ: `GET /api/permissions` (role-matrix) hoặc `GET /api/menus/me` bằng chính user dry-run.

---

## F-S9 [MED] W6 nới guard trình: trộn 1 vế PHẠM-VI-HẸP với 1 vế PHẠM-VI-RỘNG trong cùng một dòng, và tên field không khớp đĩa

`spec-wave-w6:19`: nới thành `Drafter ∨ DeptManager ∨ CreatedBy==actor ∨ Procurement`.
- `CreatedBy==actor` = hẹp, đóng đúng triệu chứng "PMH tự trình phiếu của mình".
- `Procurement` = **rộng theo role**: mọi user Cung ứng trình được HĐ nháp của **người khác**. Không có ca ÂM nào trong `spec-wave-w6:26-31` canh vế này.
- Tên field: đĩa dùng `Contract.DrafterUserId` cho mọi guard chủ-sở-hữu (`ContractFeatures.cs:302`, `:485`); `CreatedBy` là field của `BaseEntity` (audit). Spec ghi `CreatedBy` ⇒ implementer có thể so nhầm field, lệch với guard IDOR đang chạy.

**Cần:** tách 2 vế, nêu vế nào là bắt buộc; sửa tên field về `DrafterUserId` (hoặc khai rõ vì sao dùng `CreatedBy`); thêm ca ÂM nếu giữ vế `Procurement`.

---

## F-S10 [MED-LOW] `FormsController` class-trần, 0 policy per-action — hố chưa khai (đúng giả thuyết lane)

`src/Backend/SolutionErp.Api/Controllers/FormsController.cs:11` = `[Authorize]`; grep `Authorize` trong file trả **đúng 1 dòng** ⇒ không có `[Authorize(Policy=...)]` nào. `MenuKeys.Forms` ∈ All (`MenuKeys.cs:168`) nên policy `Forms.*` có sẵn — tức đây là lỗ **có sẵn thuốc mà chưa uống**. Ngoài phạm vi W8 (W8 chỉ `ContractsController`), và **không xuất hiện** trong `plan-cha §4`. Dry-run có đụng export/template HĐ ⇒ nên ít nhất khai.

---

## F-S11 [LOW] plan §4 mục 4: so sánh SỐ chặn nhiều phase hơn plan mô tả

`ContractFeatures.cs:632` `entity.Phase >= ContractPhase.DangInKy` với enum (`ContractPhase.cs:17-27`): `DangInKy=5`, `DaPhatHanh=9`, **`ChoDuyet=10`**, **`TraLai=98`**, **`TuChoi=99`**.
⇒ HĐ đang `ChoDuyet`, bị **Trả lại**, hoặc bị **Từ chối** đều KHÔNG xoá được. `plan-cha:55` chỉ nói tới "HĐ test DaPhatHanh nằm lại". Dry-run chắc chắn sinh HĐ TraLai/TuChoi (3 trạm) ⇒ rác nhiều hơn plan liệu trước; đường thoát vẫn là admin-override `:191-198` nhưng phải khai.

---

## F-S12 [LOW] Q11 "2 đường song song" = cổng KHKK bị né ĐƯỢC, và dry-run KHÔNG chứng minh được cổng giữ

`plan-cha:65` (§5 dòng 3): Q11 default "**cảnh báo mềm** — 2 đường song song (PE→HĐ tắt vẫn sống)".
Đây là quyết định nghiệp vụ hợp lệ và **đảo được**, nhưng hệ quả kiểm-soát chưa khai: trong suốt dry-run, `POST /api/contracts` (`ContractsController.cs:46`) tạo HĐ **không cần** phiếu KHKK đã duyệt ⇒ giá chốt của W3 finalize có thể bị bỏ qua. Nghĩa là **vòng dry-run không sinh được bằng chứng "cổng 3 trạm giữ"** — nó chỉ chứng minh đường thuận chạy. Nên khai ở §4 (data-risk) hoặc ghi vào acceptance E2E rằng ca ÂM "tạo HĐ không có KHKK" là ca **cố ý để mở**.

---

## F-S13 [PASS + INFO] Mật khẩu trong artifact

- **Trong phạm vi lane:** `grep -riE "Admin@1234|TestUser@|password|mật khẩu|matkhau"` trên **toàn bộ** `runs/2026-07-29-S160-khkk-dryrun-plan/` (12 file) và `runs/2026-07-28-S157-ke-hoach-ky-ket-hd/` → **0 hit**. ✅ Plan cha + 7 spec-wave + 2 sub-investigator + run.md **KHÔNG lộ mật khẩu**.
- **INFO ngoài phạm vi (không phải lỗi của bộ spec này):** `git grep -lI "Admin@123456"` = **25 file tracked**, gồm `README.md`, `docs/STATUS.md`, `docs/CLAUDE.md`, `.claude/agents/reviewer.md`, `.claude/agents/cicd-monitor.md`, `.claude/skills/iis-deploy-runbook/SKILL.md`, 5 file `.claude/agent-memory/*/MEMORY.md`. Mật khẩu admin prod nằm trong repo tracked từ lâu — quyết định của owner, chỉ nêu để không bị "vắng mặt trông giống sạch".

---

## F-S14 [PASS dương] Những chỗ chịu được soi

- "22 endpoint ghi" — đếm tay khớp 100% (11 POST + 8 PUT + 3 DELETE).
- W8 §① nói đúng vai của từng option: O-B *thu hẹp ai bấm*, O-C mới *đóng* ca Reject-sau-terminal (`:7`, `:12`). Không có claim "O-B đóng 2 ô đỏ" ⇒ **không phải lỗi**. Cảnh báo còn lại: sau O-B, 7 role được grant **vẫn** Reject/TraLai được HĐ ở mọi phase kể cả `DaPhatHanh` (`ContractWorkflowService.cs:49-66`) — spec `:12` có nói, plan `:73` có nói "O-C cần anh gật riêng" ⇒ **default KHÔNG lấn quyền owner** ở điểm này.
- Rủi ro "grant mới lật row false cố ý S92" (`spec-wave-w8:33`) — đo đĩa: revoker `RevokeTemporarilyHiddenModulesAsync` (`DbInitializer.cs:2278`, gọi ở `:2096`) nay **chỉ còn** `Hrm*`/`Off*`/`Personal` (`:2291-2303`, nhánh S92 cho Contracts/Master đã GỠ @S159). ⇒ grant `Contracts` **không đụng** vùng revoker; control-ÂM mà spec đề (`:32`) là đúng thứ cần đo. **Không phải lỗ.**
- W6 rủi ro "inbox V2 quên lọc IsDeleted" (`:32`) — `db.Contracts.AsNoTracking()` ở inbox (`ContractFeatures.cs:447`) vẫn ăn global query filter; chỉ vỡ nếu ai đó thêm `IgnoreQueryFilters` ⇒ giữ dòng cảnh báo là hợp lý, không thừa.

---

## F-S15 [LOW] Nhãn đo trong `plan-cha §3` — "curl admin 6 phase" dưới-phủ enum 12 giá trị

`plan-cha:43`: "Prod: Contracts = 0 active + 0 deleted (curl admin **6 phase** + tổng + /deleted)". `ContractPhase` có 12 giá trị (`ContractPhase.cs:17-27`: 2,3,4,5,6,7,8,9,10,98,99 + DangChon). Phép "6 phase" **không phủ** enum; vật chịu lực thật là **"tổng"** (list không filter, admin bypass `ContractFeatures.cs:298` ⇒ trả mọi phase) và `/deleted`. Kết luận "0 active" vẫn ĐÚNG nhờ "tổng", nhưng câu chữ khiến người sau tưởng phép đo là enumeration-per-phase. Nên viết lại thành "tổng không-filter (phủ mọi phase) + /deleted; 6 phase chỉ là cross-check".

---

## 3. Trả lời trực tiếp 4 câu của lane

1. **W6 có làm hở HĐ cho user ngoài workflow không?** — **CÓ RỦI RO CAO như spec đang viết** (F-S3): hàm bị sửa là role-only, 2/3 call-site không nằm trong gate, và phương án an toàn để trong ngoặc. Ca ÂM trong spec **chưa đủ** (F-S4: 0 test ÂM cho view; ca ÂM prod không pin role). **PE data thật lộ thêm gì?** — W6 tự nó không chạm PE. Đường lộ gián tiếp là qua **attachment** (F-S2): hồ sơ NCC/scan đính vào HĐ sinh từ KHKK tải được bởi mọi user đăng nhập, không cần thuộc workflow.
2. **O-B đóng đúng 2 ô đỏ không?** — **Không, và spec KHÔNG nói sai** (F-S14): `spec-wave-w8:7,:12` nói rõ O-B chỉ thu hẹp, O-C mới đóng Reject; plan `:73`/`:90` giữ O-C cho anh gật. **Seeder-grant có lật row false S92 không?** — **Không** (revoker sau S159 chỉ còn Hrm/Off/Personal). Nhưng seeder-grant có **lỗi ngược lại nguy hiểm hơn**: no-op im lặng (F-S1). **"Policy chặn theo menu-permission, user được cấp vẫn đụng HĐ THẬT" — spec khai đủ chưa?** — `spec-wave-w8:11` CÓ khai ("⚠️ thu hẹp AI, không thu hẹp BẤM VÀO ĐÂU"). Nhưng **thiếu** hệ quả cụ thể: 7 role được cấp vẫn Reject/xoá/xoá-attachment HĐ của người khác (F-S2/F-S7).
3. **plan-cha §4 còn hố nào chưa khai?** — **4 hố**: attachment upload/download/delete 0-authz 0-phase-guard (F-S2) · delete-HĐ 0 owner-check (F-S7) · `FormsController` class-trần (F-S10) · phase-guard chặn cả TraLai/TuChoi/ChoDuyet chứ không chỉ DaPhatHanh (F-S11). Cộng 1 hố "mềm": Q11 2-đường-song-song khiến dry-run không chứng minh được cổng (F-S12).
4. **Mật khẩu trong artifact?** — **0 hit** trong toàn bộ plan + 7 spec + sub-file của cả 2 run-folder ✅. Ghi nhận INFO: `Admin@123456` tồn tại ở 25 file tracked khác của repo (F-S13).

<!-- END sub-reviewer lane-5 SECURITY · 15 finding (3H/6M/5L/1PASS) · 0 anchor sai · verdict SUA-TRUOC-THI-CONG -->
