Kiểm Tra Bảo Mật Red-Team — PTX Channel Manager
Ngày: 2026-03-05 | Phạm vi: Toàn bộ mã nguồn (API, Web, DB, Hạ tầng)
Tóm Tắt Điều Hành
PTX-CM có nền tảng bảo mật vững chắc: JWT HttpOnly cookies, mã hóa thông tin đăng nhập AES-256-GCM, băm bcrypt, xoay vòng refresh token với thu hồi JTI backed-by-DB, throttling toàn cục, và Prisma ORM (không SQL injection). Tuy nhiên, tồn tại một số lỗ hổng mức trung bình đến cao mà kẻ tấn công có thể khai thác, đặc biệt ở phần gia cố hạ tầng, phân quyền cũ, và thiếu security headers.
[9-10/10] Phát Hiện NGHIÊM TRỌNG
1. Không Có Bảo Vệ CSRF
| File | apps/api/src/main.ts |
| Rủi ro | 9/10 |
| Tấn công | Cross-Site Request Forgery |
Auth sử dụng cookies sameSite: 'lax' — bảo vệ chống POST từ <form> cross-origin trên trình duyệt hiện đại, nhưng không bảo vệ chống:
- Điều hướng
GETtop-level thay đổi state (nếu có) - Subdomain thực hiện CSRF nếu cookies không có giới hạn
Domain - Fallback trình duyệt cũ
Không có CSRF token nào được tạo hoặc xác thực ở bất kỳ đâu. Không có sử dụng csrf nào trong toàn bộ API.
CAUTION
Khai thác: Kẻ tấn công tạo trang trên evil.com tự động submit form tới /api/v1/ota-accounts với PUT/DELETE. Dù sameSite: lax chặn cross-site POST trên trình duyệt hiện đại, không có phòng thủ theo chiều sâu. Nếu bất kỳ mutation endpoint nào chấp nhận tham số GET hoặc sameSite bị nới lỏng, lỗ hổng này có thể khai thác được.
Sửa: Thêm middleware csurf hoặc triển khai pattern double-submit cookie.
2. Quyền JWT Cũ — Cửa Sổ Leo Thang Đặc Quyền
| Files | apps/api/src/modules/auth/auth.service.ts, apps/api/src/common/guards/permissions.guard.ts |
| Rủi ro | 9/10 |
| Tấn công | Leo Thang Đặc Quyền / Giữ Đặc Quyền |
JWT payload chứa permissions, roleName, và country nhúng vào lúc ký (dòng 30–38). PermissionsGuard đọc permissions trực tiếp từ JWT (dòng 23), không bao giờ kiểm tra DB.
Ảnh hưởng: Nếu admin hạ cấp người dùng hoặc thay đổi vai trò:
- JWT cũ vẫn hợp lệ trong tối đa 15 phút (TTL access token)
- Xoay vòng refresh token đọc lại user từ DB (tốt), nhưng permissions được đọc lại từ vai trò DB — nên cửa sổ phơi nhiễm là 15 phút
WARNING
Khai thác: Admin thu hồi quyền DELETE của người dùng. Access token hiện tại vẫn chứa bitmask permissions cũ. Trong tối đa 15 phút, người dùng có thể xóa tài khoản OTA, properties, v.v.
Sửa: Một trong hai:
- Khi thay đổi vai trò/quyền, thu hồi tất cả refresh tokens cho người dùng bị ảnh hưởng (
revokeAllForUser) - Hoặc thêm bộ đếm phiên bản quyền: lưu
permVersiontrong JWT và trong vai trò DB; guard kiểm tra chúng khớp nhau
3. Thông Tin Đăng Nhập Production Hardcode Trong Docker Compose
| File | docker-compose.yml#L58-L61 |
| Rủi ro | 10/10 |
| Tấn công | Lộ Thông Tin Đăng Nhập |
# Dòng 58-61 — dịch vụ API production
- DATABASE_URL=postgresql://ptx_cm:ptx_cm_dev@postgres:5432/ptx_cm
- REDIS_PASSWORD=dev_redis_pwdocker-compose.yml hardcode ptx_cm_dev làm mật khẩu DB và dev_redis_pw làm mật khẩu Redis trong khối environment dịch vụ ứng dụng (dùng cho production builds). Những giá trị này đã commit vào git.
CAUTION
Dokploy compose (docker-compose.dokploy.yml) sử dụng đúng biến env ${PG_PASSWORD} và ${REDIS_PASSWORD}. Nhưng docker-compose.yml chính thì không — bất kỳ ai dùng nó để deploy sẽ có thông tin đăng nhập yếu, đã biết.
Sửa: Thay thông tin hardcode bằng tham chiếu biến ${PG_PASSWORD}. Ghi chú rằng .env phải được điền trước khi deploy.
[7-8/10] Phát Hiện MỨC CAO
4. Không Có Security Headers (Thiếu Helmet)
| File | apps/api/src/main.ts |
| Rủi ro | 7/10 |
Không sử dụng helmet trong toàn bộ API. Thiếu headers:
X-Content-Type-Options: nosniffX-Frame-Options: DENYStrict-Transport-Security(HSTS)Content-Security-PolicyX-XSS-ProtectionReferrer-Policy
Sửa: npm i helmet → app.use(helmet()) trong main.ts.
5. Nginx Chỉ Phục Vụ HTTP — Không TLS Termination
| File | nginx/nginx.conf |
| Rủi ro | 7/10 |
Nginx chỉ listen port 80. Không có HTTPS. Cờ secure: process.env.NODE_ENV === 'production' cho cookie sẽ ngăn cookies được gửi qua HTTP trong production, làm hỏng auth.
WARNING
Trong deployment hiện tại: hoặc TLS được terminate upstream (Dokploy/reverse proxy) ✅ hoặc cookies không bao giờ được gửi trong production vì Secure là true nhưng transport là HTTP ❌.
Sửa: Nếu TLS terminate upstream, thêm proxy_set_header X-Forwarded-Proto $scheme; và tin tưởng proxy. Nếu không, thêm TLS vào nginx hoặc bỏ cờ secure (không khuyến nghị).
6. Không Có Khóa Tài Khoản Sau Đăng Nhập Sai
| File | apps/api/src/modules/auth/auth.service.ts#L16-L28 |
| Rủi ro | 8/10 |
| Tấn công | Brute Force |
Endpoint đăng nhập có throttling (5 req/phút), nhưng không khóa tài khoản. Sau khi hết cửa sổ throttle, kẻ tấn công có thể thử lại vô hạn. Không có bộ đếm lần thất bại, không có delay tăng dần, không có CAPTCHA.
WARNING
Khai thác: Rate limit 5 req/phút = 7.200 lần thử/ngày. Với mật khẩu seed admin123, việc brute-force là trivial.
Sửa: Theo dõi số lần thất bại theo email trong Redis. Khóa tài khoản sau N lần thất bại. Yêu cầu CAPTCHA sau 3 lần thất bại.
[4-6/10] Phát Hiện MỨC TRUNG BÌNH
7. Chính Sách Mật Khẩu Yếu
| Files | apps/api/src/modules/users/dto/create-user.dto.ts, apps/api/src/modules/auth/dto/reset-password-with-token.dto.ts |
| Rủi ro | 6/10 |
Xác thực mật khẩu chỉ có @MinLength(8). Không yêu cầu:
- Kết hợp chữ hoa/thường
- Số hoặc ký tự đặc biệt
- Không trùng email/username
- Không nằm trong danh sách mật khẩu phổ biến
Mật khẩu seed là admin123 và staff123 — dễ đoán.
Sửa: Thêm regex @Matches() cho độ phức tạp. Thêm mustChangePassword: true cho tài khoản seed (cờ này tồn tại nhưng seed đặt mặc định false).
8. Thời Hạn Refresh Token Hardcode (Không Từ Config)
| File | apps/api/src/modules/auth/auth.service.ts#L57 |
| Rủi ro | 4/10 |
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // hardcode 7 ngàyexpiresAt trong DB luôn là 7 ngày, bỏ qua biến env JWT_REFRESH_EXPIRY. Nếu ai đó đặt JWT_REFRESH_EXPIRY=1d, JWT token hết hạn sau 1 ngày nhưng bản ghi DB tồn tại 7 ngày — tạo cửa sổ tái sử dụng token sau thời hạn dự kiến (dù JWT verification sẽ bắt lỗi).
Sửa: Parse JWT_REFRESH_EXPIRY và tính expiresAt từ đó.
9. Bypass Country Scope Cho Người Dùng Manager
| File | apps/api/src/common/guards/country-scope.guard.ts#L20-L32 |
| Rủi ro | 5/10 |
Người dùng Manager (country == null) không có country scope trừ khi tự nguyện truyền ?country=XX. Điều này có nghĩa tất cả truy vấn cho managers trả về dữ liệu tất cả quốc gia mặc định. Dù đúng về chức năng, điều này có nghĩa:
- JWT manager bị xâm phạm sẽ lộ dữ liệu TẤT CẢ quốc gia
- Không có query filtering nào được bắt buộc cho người dùng có đặc quyền
10. Thông Báo Lỗi Có Thể Lộ Thông Tin
| File | apps/api/src/common/filters/http-exception.filter.ts#L47 |
| Rủi ro | 4/10 |
console.error('[GlobalExceptionFilter] Unhandled error:', exception);Ngoại lệ non-HTTP được log với full stack traces ra stdout. Trong production, điều này có thể lộ đường dẫn nội bộ, phiên bản dependency, hoặc cấu trúc truy vấn cho bất kỳ ai có quyền truy cập log. Phản hồi API trả về generic "Internal server error" (tốt), nhưng mức độ log verbose là mối lo ngại.
11. Seed Data Chạy Trong Production Entrypoint
| File | apps/api/entrypoint.sh |
| Rủi ro | 6/10 |
Dokploy compose sử dụng entrypoint.sh có thể chạy seeds mỗi lần deploy. Nếu seed script tạo users với mật khẩu yếu (admin123), những tài khoản đó tồn tại trong production.
[1-3/10] THẤP / Thông Tin
12. Giới Hạn Body Parser 10MB
API chấp nhận payload tới 10MB (json({ limit: '10mb' })). Dù cần cho CSV imports, điều này tăng bề mặt tấn công DoS. Cân nhắc thêm giới hạn theo route.
13. Mailpit Lộ Trong Dev Compose
UI Mailpit (port 8025) và SMTP (port 1025) bind tới 127.0.0.1 — an toàn cho dev local. ✅
14. Kết Nối BullMQ Redis Không Có TLS
Kết nối Redis trong AppModule dùng TCP thuần. Nếu Redis ở segment mạng riêng, thông tin đăng nhập được truyền dạng cleartext.
15. Không Có Rate Limiting Theo Người Dùng
Throttling dựa trên IP (global ThrottlerGuard). Kẻ tấn công sau botnet/VPN có thể bypass giới hạn per-IP. Cân nhắc thêm throttling per-user-ID cho các endpoint đã xác thực.
✅ Những Điểm Làm Tốt
| Lĩnh vực | Chi tiết |
|---|---|
| Băm Mật Khẩu | bcrypt với cost factor 10 |
| JWT Cookies | HttpOnly, Secure (prod), SameSite: Lax, refresh cookie giới hạn path |
| Xoay Vòng Refresh Token | JTI backed-by-DB, token cũ xóa trước khi phát hành mới, transactional |
| Mã Hóa Thông Tin | AES-256-GCM với random 12-byte IV, auth tag được xác minh |
| Xác Thực Đầu Vào | Global ValidationPipe với whitelist: true, forbidNonWhitelisted: true |
| SQL Injection | Sử dụng Prisma ORM độc quyền — không phát hiện raw SQL |
| XSS (Server) | Không tìm thấy eval, exec, innerHTML, hoặc dangerouslySetInnerHTML |
| Docker | Non-root user, STOPSIGNAL, health checks, .dockerignore |
| Đặt Lại Mật Khẩu | Token băm SHA-256, cờ sử dụng một lần, kiểm tra hết hạn, thu hồi tất cả phiên |
| Kiểm Tra Ownership | Tài khoản OTA xác minh userId ownership trước mutations |
| Output Đã Sanitize | credentialsEncrypted và twoFactorSecret loại bỏ khỏi API responses |
| Throttling | Global 100 req/phút + giới hạn chặt hơn cho auth endpoints |
| Tắt Máy Mượt | Handlers SIGTERM/SIGINT, enableShutdownHooks() |
.gitignore | .env được exclude đúng cách |
Lộ Trình Khắc Phục Ưu Tiên
| Ưu tiên | Phát hiện | Nỗ lực |
|---|---|---|
| P0 | #3 Thông tin docker-compose hardcode | 10 phút |
| P0 | #4 Thêm Helmet | 5 phút |
| P1 | #1 Bảo vệ CSRF | 2–4 giờ |
| P1 | #2 Quyền JWT cũ (revokeAll khi thay đổi vai trò) | 2 giờ |
| P1 | #6 Khóa tài khoản | 3–4 giờ |
| P2 | #5 Nginx TLS / xác minh TLS termination upstream | 1 giờ |
| P2 | #7 Quy tắc độ phức tạp mật khẩu | 1 giờ |
| P2 | #11 Guard seed script cho production | 30 phút |
| P3 | #8 Thời hạn refresh token từ config | 15 phút |
| P3 | #9 Tài liệu hóa hành vi manager scope | 15 phút |
| P3 | #10 Structured logging cho production | 2 giờ |