Task/Plan: Runtime security guards for LumiBase
Mục tiêu
Chuẩn bị nền bảo mật runtime rõ ràng trước khi AI Harness được phép tạo hoặc áp dụng thay đổi vào hệ thống. Mỗi guard có trách nhiệm riêng, tên riêng và điểm gắn riêng trong pipeline để đội phát triển dễ hiểu, dễ review và dễ bắt buộc Harness sử dụng về sau.
Phạm vi task
-
Control-plane access guard
- Chặn principal không phải admin tại các bề mặt quản trị hệ thống như roles, policies, permissions, users, teams, settings, API keys, admin và CDC.
- Bảo vệ các bảng/collection hệ thống tương đương
lumibase_users,lumibase_roles,lumibase_permissionskhỏi thao tác leo thang đặc quyền.
-
Security headers middleware
- Gắn CSP mặc định vào mọi response.
- Bật
nosniff,DENYframe,no-referrer,Permissions-Policy, COOP và CORP để giảm XSS/CSS injection, clickjacking và lộ dữ liệu qua browser surface.
-
File upload policy
- Cấm public role tạo metadata upload hoặc đẩy bytes media thô.
- Giới hạn kích thước upload bằng
FILE_UPLOAD_MAX_BYTES, mặc định 10 MiB — kiểm tra cảContent-Lengthdo client khai VÀ số byte thật của body ở các surface nhận bytes thô (client khai gian/thiếuContent-Lengthkhông lách được). - Cho phép MIME theo
FILE_UPLOAD_ALLOWED_MIME_TYPES, mặc định chỉ gồm ảnh phổ biến, PDF, CSV và text. - Đối chiếu đuôi file khai báo với MIME type.
- Content-sniff (magic bytes) các upload bytes thô để từ chối "ảnh" thực chất là loại khác/executable; chặn thẳng mọi signature executable (PE/ELF/Mach-O).
- Từ chối SVG chứa mã động (
<script>, handleron*=,javascript:,<foreignObject>,<iframe>/<embed>,<!DOCTYPE>/<!ENTITY>XXE) — đúng ca "ảnh nhưng bị cài shell/script". - Quét toàn bộ bytes raster image (
imageHasEmbeddedActivePayload) tìm payload script/executable nhúng —<?php,<script,<html,<!doctype htmlở bất kỳ đâu — và từ chối (UPLOAD_EMBEDDED_PAYLOAD). Bắt polyglot có magic bytes ảnh hợp lệ NHƯNG kèm shell/HTML nối thêm mà kiểm tra magic-byte đầu file không thấy. Chạy đồng bộ trước khi ghi storage, trên cả hai runtime. - Các surface được phủ:
POST /api/v1/files(metadata),PUT /api/v1/files/upload/:key(bytes có chữ ký), vàPOST /api/v1/media/:key(bytes media có RBAC). Tập surface gom vềclassifyUploadSurface()và được ghim bởi test hồi quy để không thể thêm route nhận-bytes mới mà quên nối vào guard.
Cấu hình được theo site: size cap + allowlist MIME resolve theo thứ tự
DB config theo site → env override → defaultquaservices/upload-policy-service.ts(có cache, fail-safe — fallback về env/default nếu DB/cache không sẵn sàng nên guard không bao giờ fail open). Catalogue các loại chọn được nằm ở@lumibase/contracts/schemas(upload-policy.ts) để allowlist server và file picker Studio dùng chung một nguồn. Admin sửa tại Studio → Settings → Uploads, backing bởiGET/PUT /api/v1/uploads/config(GETcho mọi member để cấpacceptcho picker;PUTgatedrequireSiteAdmin, lưu vào rowsettingskeyupload_policy, chỉ nhận MIME trong catalogue).Hardening lúc serve (
routes/media.ts): file tải về trả kèmContent-Disposition: attachment+X-Content-Type-Options: nosniff, nên object HTML/SVG đã lưu không thể render (và chạy script) dưới origin app khi mở top-level. Kết hợp với CSP toàn cục (script-src 'self', khôngunsafe-inline) như phòng thủ nhiều lớp. Storage adapter mapcontentTypesang field native R2httpMetadata/ S3ContentTypeđểContent-Typekhi serve round-trip đúng (trước đây chỉ ghi vào custom metadata nên trả về undefined).Feature spec (hub):
.kiro/specs/upload-file-controls/(requirements/design/tasks) là điểm neo duy nhất cho control này và cross-ref tới mọi vị trí ở trên.Kế hoạch — re-encode ảnh để sanitize (chưa làm): cách chống polyglot không-FP là re-encode ảnh (bóc mọi thứ trừ pixel) thay vì quét marker. Việc này phụ thuộc
ImageAdapter(Sharp/CF Images) đề xuất ở.kiro/specs/image-transform-dsl/và cần quyết định sync-vs-async (re-encode async trong queuemedia-processingtạo cửa sổ file thô, đã giảm nhẹ bởi serve attachment+nosniff ở trên). Tracked là task F1 trong spec upload-file-controls; lý do bảo mật ở đây, cơ chế xử lý ảnh ở image-transform-dsl. -
Outbound URL guard
- Cung cấp utility kiểm tra outbound URL trước khi bất kỳ tính năng import/fetch URL nào gọi
fetch. - Chặn protocol không phải HTTP(S), URL có embedded credentials, localhost, RFC1918, link-local, loopback và cloud metadata IP.
- Cung cấp utility kiểm tra outbound URL trước khi bất kỳ tính năng import/fetch URL nào gọi
Kế hoạch triển khai
- Tách các thành phần thành module riêng:
security-headers,control-plane-access-guard,file-upload-policy, vàoutbound-url-guard. - Gắn
security-headersvào global middleware chain. - Gắn
control-plane-access-guardvàfile-upload-policyvào authenticated tenant-scoped API chain. - Thêm biến môi trường cấu hình upload policy trong
Bindings. - Thêm unit tests riêng cho từng guard để tên test phản ánh đúng trách nhiệm.
- Bổ sung audit event cụ thể cho các lần guard từ chối thao tác:
control_plane_access_deniedvàfile_upload_policy_denied. - Ở phase Harness tiếp theo: bắt buộc mọi tool/fetch/import do AI gọi phải đi qua
validateOutboundUrlhoặcguardedFetch. - Ở phase hardening tiếp theo: ánh xạ permission granular nếu cần cho non-admin operator.
Tiêu chí hoàn tất
- Non-admin principal nhận
CONTROL_PLANE_FORBIDDENkhi gọi system control-plane routes. - Public role không thể tạo file metadata upload hoặc đẩy bytes media thô.
- Upload quá kích thước (theo Content-Length khai báo hoặc theo body thật), dùng
MIME ngoài allowlist, sai đuôi/nội dung so với khai báo, hoặc SVG có mã động
đều bị từ chối trước khi ghi storage — trên mọi surface được phủ, gồm cả
POST /api/v1/media/:key. - File media tải về được serve dạng attachment kèm
nosniff, vàContent-Typeround-trip đúng qua cả hai storage adapter. - Mọi response có CSP và các security headers nền tảng.
- Outbound URL guard có test cho localhost, private IP, link-local metadata IP và protocol nguy hiểm.
- Control-plane guard và file upload policy ghi audit event riêng khi request có DB context.
Guard: RBAC context cho ItemService (chống fail-open)
Vấn đề
ItemService chỉ enforce row/field RBAC khi được khởi tạo kèm permissionCtx.
Nếu vắng, this.permissions = null và mọi kiểm tra quyền short-circuit sang
"cho phép" (fail-open). Thiết kế này cố ý — worker hệ thống chạy hợp lệ mà không
có user principal — nhưng khiến một call site theo request nếu quên
permissionCtx sẽ âm thầm bỏ qua phân quyền, trông y hệt system context cố ý.
Lỗi này từng ship: skill AI updateItem chạy ItemService.patch() trên service
thiếu permissionCtx → LLM sửa item bỏ qua RBAC (PR #151). Rà soát lại phát hiện
endpoint MCP (routes/mcp.ts) dính đúng lỗi này dù comment của nó cam kết
"MCP client không thể làm nhiều hơn token qua Agent API".
Cơ chế chặn hồi quy (đã triển khai)
Mọi khởi tạo ItemService đi qua hai helper tường minh trong
apps/cms/src/services/item-service-factory.ts:
itemServiceForRequest(c)— dùng cho MỌI service tạo khi xử lý HTTP request (routes, GraphQL resolver, MCP). Luôn gắnpermissionCtxtừ Hono context;permissionCtxáp cuối cùng nênoverrideskhông thể vô tình gỡ enforcement.itemServiceForSystem(deps, reason)— dùng cho flow hệ thống/nền. Bắt buộc truyềnSystemContextReason('scheduler'|'background-worker'|'compliance-erasure') để tác giả phải nêu lý do tại sao fail-open an toàn.
Test hồi quy apps/cms/src/__tests__/item-service-rbac-context.test.ts quét source
và fail CI nếu có new ItemService(...) trực tiếp ngoài factory (trừ allowlist
đã review). Người sửa sau không thể tái tạo lỗ hổng fail-open mà reviewer không thấy.
Bảng phân loại call site (audit)
| Call site | Chế độ | Lý do |
|---|---|---|
routes/items.ts | request | REST CRUD — enforce RBAC theo bearer token |
routes/ai.ts (/chat, /approvals/:id/decide) | request | skill AI enforce cùng RBAC như /items (PR #151) |
routes/mcp.ts | request | đã vá — trước đây thiếu permissionCtx (fail-open) |
routes/admin-sar.ts | request | SAR export — admin-gated + enforce RBAC |
graphql/context.ts | request | GraphQL kế thừa nguyên governance của REST |
services/scheduler-worker.ts | system scheduler | cron retention sweep, không có user principal |
services/veto-commit-worker.ts | system background-worker | commit sau khi veto window đã chốt quyết định human |
services/agent-run-worker.ts | system background-worker | governed agent run — HITL/autonomy gate ở harness, không phải RBAC user |
services/erasure-service.ts | system compliance-erasure | erasure/SAR gated admin + dual-control ở tầng service |
Checklist khi thêm ItemService mới
- Call site nằm trong đường xử lý HTTP request (có
Context<AppEnv>)? → dùngitemServiceForRequest(c). TUYỆT ĐỐI không tự viếtpermissionCtxinline. - Call site là worker/cron/compliance chạy quyền hệ thống? → dùng
itemServiceForSystem(deps, reason)và chọnreasonđúng ngữ nghĩa. - Nếu buộc phải
new ItemService(...)trực tiếp (hiếm) → thêm vàoALLOWED_DIRECT_CONSTRUCTIONtrong test guard kèm lý do, để reviewer thấy. - Đã cập nhật bảng phân loại phía trên khi thêm call site request/system mới.
Tiêu chí hoàn tất (guard này)
- Không file production nào (ngoài factory + allowlist) gọi
new ItemService(...)trực tiếp — bảo đảm bằng test source-scan. - Endpoint AI và MCP enforce row/field RBAC đúng như
/items(skill LLM không leo quyền vượt token). - Mọi system context khai báo
reasontường minh, greppable, có mặt trong bảng audit.