Skip to content

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

Fileapps/api/src/main.ts
Rủi ro9/10
Tấn côngCross-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 GET top-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

Filesapps/api/src/modules/auth/auth.service.ts, apps/api/src/common/guards/permissions.guard.ts
Rủi ro9/10
Tấn côngLeo 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:

  1. 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)
  2. Hoặc thêm bộ đếm phiên bản quyền: lưu permVersion trong 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

Filedocker-compose.yml#L58-L61
Rủi ro10/10
Tấn côngLộ Thông Tin Đăng Nhập
yaml
# 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_pw

docker-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}${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)

Fileapps/api/src/main.ts
Rủi ro7/10

Không sử dụng helmet trong toàn bộ API. Thiếu headers:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Strict-Transport-Security (HSTS)
  • Content-Security-Policy
  • X-XSS-Protection
  • Referrer-Policy

Sửa: npm i helmetapp.use(helmet()) trong main.ts.


5. Nginx Chỉ Phục Vụ HTTP — Không TLS Termination

Filenginx/nginx.conf
Rủi ro7/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ì Securetrue 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

Fileapps/api/src/modules/auth/auth.service.ts#L16-L28
Rủi ro8/10
Tấn côngBrute 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

Filesapps/api/src/modules/users/dto/create-user.dto.ts, apps/api/src/modules/auth/dto/reset-password-with-token.dto.ts
Rủi ro6/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 seedadmin123staff123 — 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)

Fileapps/api/src/modules/auth/auth.service.ts#L57
Rủi ro4/10
typescript
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // hardcode 7 ngày

expiresAt 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

Fileapps/api/src/common/guards/country-scope.guard.ts#L20-L32
Rủi ro5/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

Fileapps/api/src/common/filters/http-exception.filter.ts#L47
Rủi ro4/10
typescript
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

Fileapps/api/entrypoint.sh
Rủi ro6/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ựcChi tiết
Băm Mật Khẩubcrypt với cost factor 10
JWT CookiesHttpOnly, Secure (prod), SameSite: Lax, refresh cookie giới hạn path
Xoay Vòng Refresh TokenJTI backed-by-DB, token cũ xóa trước khi phát hành mới, transactional
Mã Hóa Thông TinAES-256-GCM với random 12-byte IV, auth tag được xác minh
Xác Thực Đầu VàoGlobal ValidationPipe với whitelist: true, forbidNonWhitelisted: true
SQL InjectionSử 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
DockerNon-root user, STOPSIGNAL, health checks, .dockerignore
Đặt Lại Mật KhẩuToken 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 OwnershipTài khoản OTA xác minh userId ownership trước mutations
Output Đã SanitizecredentialsEncryptedtwoFactorSecret loại bỏ khỏi API responses
ThrottlingGlobal 100 req/phút + giới hạn chặt hơn cho auth endpoints
Tắt Máy MượtHandlers SIGTERM/SIGINT, enableShutdownHooks()
.gitignore.env được exclude đúng cách

Lộ Trình Khắc Phục Ưu Tiên

Ưu tiênPhát hiệnNỗ lực
P0#3 Thông tin docker-compose hardcode10 phút
P0#4 Thêm Helmet5 phút
P1#1 Bảo vệ CSRF2–4 giờ
P1#2 Quyền JWT cũ (revokeAll khi thay đổi vai trò)2 giờ
P1#6 Khóa tài khoản3–4 giờ
P2#5 Nginx TLS / xác minh TLS termination upstream1 giờ
P2#7 Quy tắc độ phức tạp mật khẩu1 giờ
P2#11 Guard seed script cho production30 phút
P3#8 Thời hạn refresh token từ config15 phút
P3#9 Tài liệu hóa hành vi manager scope15 phút
P3#10 Structured logging cho production2 giờ

PTX Channel Manager — Tài Liệu Nội Bộ