SHOPV3 - SECURITY PATCH 2026-08-25
================================

MỤC ĐÃ VÁ
----------
1) Fpayment callback
- Callback completed được xử lý trong DB transaction + lockForUpdate.
- Callback hợp lệ gửi lặp/song song chỉ được cộng tiền một lần.
- Trạng thái invoice được kiểm tra lại sau khi lock.
- api_key của Fpayment không còn bị lưu vào transactions.extras.
- Không trả exception message nội bộ cho client khi lỗi 500.
- So sánh merchant credential bằng hash_equals.

2) Staff order authorization / race condition
- Staff không thể GET credential của đơn Pending trước khi claim.
- Pending list không expose admin_note/credential hidden fields.
- Claim Item/Boosting là atomic conditional update; hai staff claim cùng lúc chỉ một người thành công.
- Luồng Cancel/Refund Item/Boosting dùng transaction + row lock để tránh refund lặp.
- Allowlist sort_by cho API staff.

3) Stored XSS
- order_note trong DataTables staff được HTML-escape.
- code/name/group/status fallback ở các bảng liên quan cũng được escape trước khi ghép HTML.

4) SSRF / outbound request
- Thêm App\Support\Security\OutboundUrlPolicy.
- Bank url_api và Provider api_url: HTTPS only, port 443, không userinfo, chặn IP private/loopback/reserved.
- Outbound HTTP tắt redirect và có connect timeout.
- Provider đổi hostname bắt buộc nhập token mới để không gửi token cũ sang host mới.
- Có thể cấu hình allowlist host bằng BANK_API_ALLOWED_HOSTS / PROVIDER_ALLOWED_HOSTS.

5) Cron/scheduler
- Cron nội bộ mới nằm dưới /schedule/internal/*.
- State-changing cron dùng POST, không dùng GET.
- Secret gửi qua Authorization: Bearer hoặc X-Cron-Key, không nằm trong URL.
- Có optional CRON_ALLOWED_IPS.
- Route cron URL-key cũ tắt mặc định. Chỉ bật tạm bằng CRON_ALLOW_LEGACY_URL_KEY=true.
- Callback thanh toán bên thứ ba vẫn giữ URL tương thích vì gateway có thể đang lưu callback cũ.

6) Secrets trong source bundle
- File ZIP phát hành này KHÔNG chứa .env production.
- .env.example đã được thay bằng template sạch, không còn copy secret production.
- Thêm .gitignore để chặn commit .env.

CÁCH DEPLOY AN TOÀN
-------------------
1. Backup source + database hiện tại.
2. KHÔNG xóa file .env hiện tại trên server.
3. Upload/giải nén source patch đè code. File ZIP patch không chứa .env nên .env hiện tại của server được giữ nguyên.
4. Xóa cache cấu hình sau deploy:
   php artisan optimize:clear

5. Cron mới (ví dụ cron tổng mỗi phút):
   * * * * * curl -sS -X POST -H "Authorization: Bearer YOUR_CRON_SECRET" "https://YOUR-DOMAIN/schedule/internal/run-all" > /dev/null 2>&1

   YOUR_CRON_SECRET:
   - Nếu bạn đặt CRON_AUTH_SECRET trong .env: dùng giá trị đó.
   - Nếu CRON_AUTH_SECRET để trống: code fallback sang project key hiện tại.

6. Khuyến nghị thêm vào .env:
   CRON_AUTH_SECRET=<random-secret-dai>
   CRON_ALLOW_LEGACY_URL_KEY=false
   CRON_ALLOWED_IPS=
   BANK_API_ALLOWED_HOSTS=
   PROVIDER_ALLOWED_HOSTS=

   BANK_API_ALLOWED_HOSTS / PROVIDER_ALLOWED_HOSTS là danh sách host phân tách bằng dấu phẩy.
   Ví dụ:
   PROVIDER_ALLOWED_HOSTS=api.provider-a.com,api.provider-b.com

7. Nếu cron cPanel cũ chưa kịp đổi, có thể TẠM bật:
   CRON_ALLOW_LEGACY_URL_KEY=true
   sau đó php artisan optimize:clear
   Đổi xong cron thì trả lại false ngay.

8. Vì archive cũ từng chứa .env thật, hãy rotate ít nhất:
   - DB_PASSWORD
   - payment/provider API secrets
   - webhook/cron/project secrets
   APP_KEY không đổi bừa: phải kiểm kê dữ liệu Laravel đã encrypt trước khi rotate.

LƯU Ý TƯƠNG THÍCH
-----------------
- URL Bank/Provider dùng HTTP, private IP hoặc port khác 443 sẽ bị chặn có chủ đích.
- Nếu Provider/Bank hợp lệ nhưng bị policy từ chối, dùng HTTPS:443 và (tốt nhất) thêm hostname vào allowlist .env.
- Callback Fpayment/PM/Card vẫn cần prj_key trong callback URL để không phá cấu hình đã lưu ở gateway.
- window.sanctumToken vẫn tồn tại để giữ tương thích frontend cũ. Stored XSS đã được vá tại sink đã xác nhận, nhưng về dài hạn nên chuyển first-party AJAX sang cookie/stateful Sanctum để giảm blast radius nếu xuất hiện XSS mới.
- CSP strict nonce chưa bật cưỡng chế vì source hiện có nhiều inline script; bật ngay có thể làm hỏng admin/staff UI. Nên triển khai CSP Report-Only rồi migrate script dần.

KIỂM TRA ĐÃ CHẠY TRONG SANDBOX
------------------------------
- PHP lint cho các file PHP sửa đổi: PASS.
- php artisan route:list --except-vendor: PASS.
- Blade compileString + php -l cho các Blade sửa đổi: PASS.
- OutboundUrlPolicy: http://, 127.0.0.1 và ::1 bị reject: PASS.
- php artisan view:cache không chạy được trong sandbox do PHP thiếu extension DOMDocument; đây là giới hạn runtime sandbox, không phải lỗi syntax Blade.

RETEST NÊN CHẠY TRÊN STAGING
----------------------------
- Gửi cùng callback Fpayment 10 lần song song -> balance chỉ tăng 1 lần.
- Hai staff claim cùng một order song song -> một 200, một 409.
- Staff chưa claim GET /api/staff/.../{id} -> 403.
- order_note = <img src=x onerror="window.__XSS=1"> -> hiển thị text, JS không chạy.
- Bank/Provider URL = https://127.0.0.1 hoặc https://[::1] -> reject.
- GET /schedule/internal/run-all -> 405.
- POST không Bearer -> 401.
- POST Bearer đúng -> cron chạy.
