INTERNAL DEV ONLY Security Audit Report (SEC-01..16 & INFRA-01..03) (WBS 2.36.1 & 2.37.1)

Security Audit Report (Gotrace-portal — kl-audit v3.0) ​


1. Executive Summary ​

Tuy nhiên, quá trình Security Audit đã xác thực 16 lỗ hổng code & kiến trúc (SEC-01 đến SEC-16) và 3 hạng mục Dependency / Infrastructure (INFRA-01 đến INFRA-03), bao gồm 5 CRITICAL và 5 HIGH:

Finding ID Vulnerability Title Severity CVSS v3.1 CWE / Reference
SEC-01 Broken Maker-Checker & Missing AdminGuard trên /api/v1/registrations: Any Authenticated User Can Approve Registrations, Hijack Existing Enterprise via Tax Code & Read Plaintext Password CRITICAL 9.8 CWE-284, CWE-862 / OWASP API5:2023
SEC-02 Cross-Tenant BOLA/BFLA tại UsersController: Reset Password/Phone của User Bất kỳ, Privilege Escalation AGENT -> ADMIN, Arbitrary Role Assignment & Leak Password Hash qua get-staffs-by-company CRITICAL 9.6 CWE-269, CWE-639 / OWASP API1:2023, API5:2023
SEC-03 Cross-Tenant Leak NDATrace Token (companies.token) qua GET /api/v1/companies/:id & Mass Assignment trên Company Profile CRITICAL 9.1 CWE-200, CWE-915 / OWASP API3:2023
SEC-04 Cross-Tenant BOLA/IDOR trên 10 Modules (batches push dữ liệu giả lên National Trace Gateway, qr-codes, stamps đọc & recall tem, raw-material-zones, warehouses, company-certifications, input-sources, shipment-orders, vehicles, drivers) & BFLA trên 4 Admin/QLNN Modules (personnel, lookups, consultation-request, oversight-officers) CRITICAL 9.1 CWE-639, CWE-862 / OWASP API1:2023, API5:2023
SEC-05 Committed 9 SQL Database Dumps (docs/db/*.sql), prisma/seed.ts Upsert Reset Default Password cho admin@gmail.com (role:1) trong deploy-prod.sh, 6.977 Bcrypt Hashes trong HanoiTrace Seed & Unenforced must_change_password CRITICAL 9.1 CWE-798, CWE-200 / NĐ 13/2023/NĐ-CP
SEC-06 Stored XSS qua GTIN Description (normalizeRichHtml trên Public QR Trace & rehypeRaw trên Admin GTIN Table) Kết hợp Non-HttpOnly Token Cookie HIGH 8.6 CWE-79, CWE-1004 / OWASP ASVS V5.3.3
SEC-07 Twin-Service Security Drift tại apps/zalo-be: Wildcard CORS + Credentials, Missing Auth Rate-Limit, Bypass Subscription Service Gate, Pass-Through JwtStrategy & Unchecked ENTERPRISE Refresh Token, 9-Digit Suffix Phone Matching HIGH 8.3 CWE-942, CWE-307, CWE-287 / OWASP API2:2023
SEC-08 trust proxy: true & Raw X-Forwarded-For Parsing: Rate-Limit Bypass, 24h Victim IP Ban qua Honeypot includes(), SSR XFF Forwarding & Exposed NEXT_PUBLIC_ANTI_CRAWL_SECRET HIGH 7.5 CWE-290, CWE-400 / OWASP API4:2023
SEC-09 RBAC Temporal Validity Bypass: JwtStrategy.validate() Bỏ qua Kiểm tra valid_from / valid_to của RoleAssignment Cho Tới Khi Cron Chạy (≤ 24h) MEDIUM 6.8 CWE-285, CWE-672 / OWASP API5:2023
SEC-10 Short-Circuit Evaluation (||) Bypass Magic-Byte Validation trong MinioStorageService / S3StorageService & inline Content-Disposition tại PublicFilesController MEDIUM 6.8 CWE-434 / OWASP ASVS V12.1.1
SEC-11 Session Management & CSRF Gaps: Refresh Token Không Revoke Khi Logout, changePassword Không Increment token_version, WebSocket/BullBoard Không Check tv & origin.startsWith Prefix Bypass MEDIUM 6.8 CWE-352, CWE-613 / OWASP ASVS V3.3.2
SEC-12 Unauthenticated NDATrace Webhook /api/v1/webhook: Replay id Ngoài Ciphertext, Ambiguous Company Lookup OR(taxCode|name|email) & Static ECDH Thiếu KDF/AAD MEDIUM 6.5 CWE-306, CWE-294, CWE-327 / OWASP ASVS V2.9.1
SEC-13 Unvalidated Dynamic orderBy Injection tại input-sources, batchs, users, registrations & Leak Raw Prisma exception.message khi HTTP 500 MEDIUM 6.5 CWE-943, CWE-209 / OWASP API3:2023
SEC-14 Unprotected Pre-Auth Cost-Sinks (Missing Turnstile/Captcha, Missing Global Circuit Breaker cho Email/VietQR/MinIO, Missing Multer fileSize Limit trên POST /registrations) & User Enumeration MEDIUM 5.8 CWE-204, CWE-400 / OWASP API4:2023
SEC-15 Weak Default Credentials & Fail-Open ${VAR:-default} Fallbacks trong .env.staging.example, docker-compose.staging.yml, docker/docker-compose.yml (Bind 0.0.0.0) & Unchecked JWT_SECRET Entropy MEDIUM 5.3 CWE-1188 / OWASP ASVS V14.2.1
SEC-16 Cross-Service Exploit Chain: Cross-Tenant Phone Update trên backend (PUT /users/:id) Kết hợp Auto-Bind zalo_user_id trên zalo-be Chiếm Quyền Vĩnh viễn Enterprise Nạn nhân HIGH 8.5 CWE-287, CWE-639 / OWASP API2:2023
INFRA-01 92 Runtime-Reachable Dependency Vulnerabilities trong yarn.lock (next 16.1.6, axios 1.13.6, body-parser 2.2.2, path-to-regexp 8.3.0, proxy-addr 2.0.7, multer 2.1.1, nodemailer, xlsx 0.18.5, js-cookie 3.0.5) HIGH — OWASP A06:2021 — Vulnerable Components
INFRA-02 Container Security Hardening (4 Dockerfiles thiếu USER node chạy dưới quyền root, bundle kèm devDependencies) LOW — CIS Docker Benchmark
INFRA-03 Production Infrastructure Verification Checklist (Cloudflare/Nginx CF-Connecting-IP & INTERNAL_API_URL, Rotate companies.token với NDATrace, Verify DB Passwords & admin@gmail.com) VERIFY — Defense-in-Depth

2. Technical Deep Dive & Remediation Guide (SEC-*) ​

[SEC-01] Broken Maker-Checker trên /api/v1/registrations — Enterprise Account Takeover (CRITICAL — CVSS 9.8) ​

Location ​

Root Cause & Impact ​

  1. Các admin routes trong RegistrationsController (GET /stats, GET /:id, GET /, PUT /:id/approve, PUT /:id/reject) chỉ gắn @UseGuards(JwtAuthGuard) mà không có AdminGuard hay @CheckPermissions. Vì PermissionsGuard cho qua mọi route không gắn metadata, bất kỳ user nào đã đăng nhập (kể cả tài khoản PARENT_REP, STORE_STAFF, hoặc tài khoản đang bị SUSPENDED) đều gọi được API duyệt đơn PUT /api/v1/registrations/:id/approve!
  2. Trong RegistrationsService.create() (dòng 184–237), hệ thống chỉ check trùng tax_code trên bảng registrations mà không check bảng companies.
  3. Khi gọi RegistrationsService.approve() (dòng 588–603):
    typescript
    const existingCompany = await this.prisma.company.findFirst({
      where: { company_tax_code: registration.tax_code },
      select: { id: true },
    });
    if (existingCompany) {
      companyId = existingCompany.id;
      await this.prisma.company.update({
        where: { id: companyId },
        data: { organization_types: registration.organization_types },
      });
    }
    Hàm approve() lập tức tái sử dụng existingCompany.id của doanh nghiệp đang hoạt động có cùng MST, tạo một user mới gắn quyền chủ doanh nghiệp (ENT_OWNER / HIEU_TRUONG / SCHOOL_ADMIN) vào chính doanh nghiệp đó, và trả thẳng plaintext password (generated_password: randomPassword) trong HTTP response (dòng 690)!

Remediation ​

  1. Bổ sung AdminGuard vào toàn bộ các admin routes trong registrations.controller.ts:
    typescript
    @UseGuards(JwtAuthGuard, AdminGuard)
  2. Trong RegistrationsService.approve(), chặn việc tự động gắn owner mới vào doanh nghiệp đã có thành viên, tăng độ dài random password và không trả generated_password trong HTTP response:
    typescript
    if (existingCompany) {
      const memberCount = await this.prisma.user.count({
        where: { company_id: existingCompany.id, deleted_at: null },
      });
      if (memberCount > 0) {
        throw new ConflictException(
          'Doanh nghiệp đã có tài khoản chủ sở hữu — cần xác minh thủ công',
        );
      }
      companyId = existingCompany.id;
    }

[SEC-02] Cross-Tenant BOLA/BFLA tại UsersController & Privilege Escalation AGENT -> ADMIN (CRITICAL — CVSS 9.6) ​

Location ​

Root Cause & Impact ​

  1. Cross-Tenant User Read, Update, Password Reset, Lock & Delete:
    • GET /api/v1/users/:id (findDetail), PUT /api/v1/users/:id (update), DELETE /api/v1/users/:id (remove), và PATCH /api/v1/users/:id/toggle-status (toggleStatus) không kiểm tra oldUser.company_id có thuộc tenant hoặc unit_path của currentUser hay không. Một admin của Enterprise A có thể reset password (dòng 807–809, không increment token_version) hoặc khóa tài khoản của Enterprise B hoặc cán bộ QLNN!
  2. Privilege Escalation từ AGENT lên ADMIN (role: 1):
    • Trong PermissionsGuard (permission.guard.ts:35), cả ROLE.ADMIN và ROLE.AGENT đều bypass kiểm tra @CheckPermissions.
    • Trong UsersService.update() (dòng 730–740) và create() (dòng 518–553), code chỉ chặn currentUser.role === ROLE.BUSINESS tạo role khác và chặn sửa ROLE.GOVERNMENT, nhưng không chặn ROLE.AGENT truyền role: 1 (ROLE.ADMIN) để tự nâng quyền chính mình hoặc tạo tài khoản ADMIN toàn hệ thống!
    • Ngoài ra, update() không gọi validateRoleAssignmentSubset() khi cập nhật role_assignment.role_id (dòng 823–867), và bản thân validateRoleAssignmentSubset() (dòng 1075–1105) chỉ kiểm tra allowed_account_types chứ chưa kiểm tra permissions subset.
  3. Password Hash Disclosure qua GET /api/v1/users/get-staffs-by-company/:companyId:
    • Route users.controller.ts:109-115 không gắn @CheckPermissions và gọi userRepository.findAll() không giới hạn select, trả về toàn bộ các cột của bảng users bao gồm cả bcrypt password hash của mọi nhân sự thuộc companyId bất kỳ!

Remediation ​


[SEC-03] Cross-Tenant Leak NDATrace Token (companies.token) & Mass Assignment trên Company Profile (CRITICAL — CVSS 9.1) ​

Location ​

Root Cause & Impact ​

  1. CompaniesController chỉ gắn @UseGuards(JwtAuthGuard). Hai endpoint GET /api/v1/companies/:id (findDetail) và GET /api/v1/companies/:id/dashboard (getDashboard) không nhận @CurrentUser(), không check tenant isolation và query toàn bộ scalar columns của bảng companies — trả thẳng trường token (khóa xác thực tích hợp NDATrace của doanh nghiệp), did, và webhook_id cho bất kỳ user nào đã đăng nhập!
  2. Tại PUT /api/v1/companies/:id (companies.service.ts:874-943), biến isBusinessManager chỉ giới hạn ENT_OWNER và HIEU_TRUONG vào { organization_types }, còn mọi role khác (như ROLE.GOVERNMENT hoặc ROLE.AGENT, vì route không gắn @CheckPermissions nên rule chặn write đối với GOVERNMENT trong PermissionsGuard không chạy) đều rơi vào nhánh : companyData, cho phép ghi đè trực tiếp token, did, status, register_status, gcp, sale_user_id của bất kỳ doanh nghiệp nào!

Remediation ​


[SEC-04] Cross-Tenant BOLA/IDOR (10 Modules) & BFLA (4 Admin/QLNN Modules) (CRITICAL — CVSS 9.1) ​

Location & Impact ​

  1. Batches (BatchController & BatchService — Exploit Chain CH-2 Forged National Trace Sync):
    • apps/backend/src/modules/batchs/batch.controller.ts:61, 106-109, 147-148, 166-170, 182-183, 206-207, 230-231, 310-320 & batch.service.ts:605-686, 1943-1975
    • PUT /api/v1/batches/:id cập nhật batch của doanh nghiệp khác mà không check company_id, sau đó tự động gọi enqueueIntegrationEvent('batch.updated') (batch.service.ts:680-684) -> kích hoạt batchUpdatedPush (apps/integration-gateway/src/connectors/txng-national/index.ts:59) đẩy bản tin PUT LenhSanXuat giả mạo lên Cổng TXNG Quốc gia dưới danh nghĩa doanh nghiệp nạn nhân!
    • GET/POST /api/v1/batches/:id/export-sgtins tải toàn bộ mã SGTIN & QR URLs của doanh nghiệp khác; getHistory, findInventoryByBatch, findAll, getBatchByMaterial, getAllSgtin thiếu check company_id.
  2. Stamps (StampsController & StampsService):
    • apps/backend/src/modules/stamps/stamps.controller.ts:168-184, 221-285 & stamps.service.ts:438-499, 961-1116
    • Mặc dù model Stamp (schema.prisma:1032) có trực tiếp cột company_id BigInt, cả 2 hàm đọc (findOne, getHistory) và 5 hàm state mutation (suspendStamp, reactivateStamp, recallStamp, markDefective, reprintStamp) đều không check stamp.company_id. Đối thủ có thể gọi PUT /api/v1/stamps/:id/recall để recall tem của doanh nghiệp khác!
  3. QR Codes (QrCodesController & QrCodesService): apps/backend/src/modules/qr-codes/qr-codes.controller.ts:90-112 (GET /:batchId và DELETE /:batchId bỏ sót resolveBatchWithOwnerCheck).
  4. Raw Material Zones (RawMaterialZoneController): apps/backend/src/modules/raw-material-zones/raw-material-zone.controller.ts:167-177, 195-205 (GET /:id và DELETE /:id xóa vùng nguyên liệu của tenant khác).
  5. Company Certifications (CompanyCertificationsController): apps/backend/src/modules/company-certifications/company-certifications.controller.ts:171-181, 199-209 (GET /:id và DELETE /:id xóa chứng chỉ của tenant khác).
  6. Input Sources (InputSourcesService): apps/backend/src/modules/input-sources/input-sources.service.ts:391-397, 596-601 (link company_certification_ids của tenant khác vào input source của mình).
  7. Warehouses (WarehousesController): apps/backend/src/modules/warehouses/warehouses.controller.ts:155-221 (GET /:id và 6 route GET /:id/inventory/*, transactions, stock-take-items thiếu @CheckPermissions và thiếu check company_id, trong khi bản twin trên apps/zalo-be lại có check company_id).
  8. Shipment Orders, Vehicles, Drivers:
    • apps/backend/src/modules/shipment-orders/shipment-orders.controller.ts:146-155 (GET /:id)
    • apps/backend/src/modules/vehicles/vehicles.controller.ts:83-92 (GET /:id)
    • apps/backend/src/modules/drivers/drivers.controller.ts:83-92 (GET /:id)
  9. Inspector Personnel, System Lookups, Oversight Officers & Consultation Requests (BFLA):
    • apps/backend/src/modules/personnel/personnel.controller.ts:16-133 & apps/backend/src/modules/lookups/lookups.controller.ts:16-106: Chỉ dùng AdminOrActiveCompanyGuard (cho phép mọi user ROLE.BUSINESS đang active), dẫn tới nhân viên doanh nghiệp bất kỳ có thể thêm/sửa/xóa hồ sơ Cán bộ Thanh tra Nhà nước (/api/v1/personnel) và sửa/xóa extra_data của cây Đơn vị hành chính (/api/v1/lookups/items/:id) để thao túng danh sách cán bộ giám sát bếp ăn (getOversightOfficers)!
    • apps/backend/src/modules/users/users.controller.ts:117-124 (GET /api/v1/users/oversight-officers): Thiếu @CheckPermissions, lộ danh sách cán bộ giám sát cho mọi user.
    • apps/backend/src/modules/consultation-request/consultation-request.controller.ts:8-26: Chỉ dùng JwtAuthGuard mà không có AdminGuard cho /list và /edit.
    • Suspended Account Bypass: Vì hasValidAssignment chỉ được check bên trong PermissionsGuard (permission.guard.ts:71), mọi tài khoản đã bị SUSPENDED vẫn gọi được tất cả các route không gắn @CheckPermissions!

[SEC-05] Committed 9 SQL Dumps (docs/db/*.sql), prisma/seed.ts Reset Default ADMIN Password trong deploy-prod.sh & 6.977 Bcrypt Hashes trong Seed (CRITICAL — CVSS 9.1) ​

Location ​

  1. docs/db/stg_gotrace_1006.sql (26.5 MB), docs/db/gotrace_stg_0605.sql (14.6 MB), docs/db/gotrace_master_data.sql, docs/db/gotrace_0506.sql, docs/db/gotrace_0304.sql, docs/db/gotrace_3003.sql, docs/db/import_data.sql: Chứa companies.token, ~185 bcrypt password hashes, PII người tiêu dùng/cán bộ và chữ ký tay (shipment_pickups.*_signature).
  2. apps/backend/prisma/seed.ts:29-53, apps/zalo-be/prisma/seed.ts:16-26, deploy-prod.sh:709-718, 1226:
    • seed.ts chứa hardcoded default password, thực hiện prisma.user.upsert cho tài khoản admin@gmail.com (role: 1 — ROLE.ADMIN) với update: { password: hashedPassword } (overwrite password ADMIN về mặc định mỗi lần chạy seed!) và in password ra stdout. Nguy hiểm hơn, script deploy-prod.sh có tích hợp lệnh chạy seed và tài khoản admin@gmail.com có mặt trong cả 7 bản dump SQL!
  3. apps/backend/src/common/scripts/data/hanoitrace-*.data.ts (6.977 bcrypt hashes của 3.959 nhà cung cấp, 2.099 trường học, 756 phòng ban xã/phường, 126 xã/phường, 25 cơ quan NN, 12 UBND) kết hợp với việc jwt.strategy.ts:57-211 không enforce cờ must_change_password.
  4. Git history (commit 4503f0ea860812f9b3d3fc4c81e54cc1c616cd6a): .claude/settings.local.json:67-72 chứa live Bearer JWT.

Remediation ​


[SEC-06] Stored XSS qua GTIN Description trên Public QR Trace & Admin GTIN Table Kết hợp Non-HttpOnly Token Cookie (HIGH — CVSS 8.6) ​


[SEC-07] Twin-Service Security Drift tại apps/zalo-be (HIGH — CVSS 8.3) ​

  1. apps/zalo-be/src/main.ts:36: app.enableCors({ origin: true, credentials: true }) reflect mọi Origin kèm credentials.
  2. apps/zalo-be/src/app.module.ts:53-54: Không đăng ký ThrottlerGuard làm APP_GUARD.
  3. apps/zalo-be/src/modules/auth/auth.service.ts:353-427, 549-593: Hàm buildRoles() và switchToEnterprise() không check trạng thái dịch vụ của doanh nghiệp (companies.status / enforceServiceGate), cho phép doanh nghiệp hết hạn subscription bypass qua Zalo.
  4. apps/zalo-be/src/modules/auth/auth.service.ts:282-340 & jwt.strategy.ts:23-28: Hàm refresh() chỉ re-validate DB cho DRIVER và CONSUMER, bỏ qua hoàn toàn role ENTERPRISE, đồng thời JwtStrategy chỉ pass-through payload mà không check DB hay token_version.
  5. apps/zalo-be/src/modules/auth/auth.service.ts:51-53, 360, 368, 436, 473-480 & zalo-webhook.service.ts:17-40: Match SĐT bằng 9 số cuối (endsWith) và thiếu kiểm tra replay window trên Webhook Zalo.

[SEC-08] trust proxy: true & Raw X-Forwarded-For Parsing: Rate-Limit Bypass & 24h Victim IP Ban (HIGH — CVSS 7.5) ​


[SEC-09] đến [SEC-16] & Infrastructure Findings (INFRA-01 .. INFRA-03) ​