Join hàng triệu room/channel để gây memory exhaustion.
Event injection, prototype pollution, SQL/NoSQL injection hoặc XSS.
Token vẫn dùng được sau logout hoặc thay đổi quyền.
Không có một middleware đơn lẻ giải quyết tất cả. Phòng thủ phải nhiều tầng.
Authentication khác Authorization
Authentication: connection này đại diện cho ai?
Authorization: identity đó có được thực hiện action này trên resource này không?
Xác minh JWT ở handshake chỉ trả lời câu hỏi đầu tiên. Một user đăng nhập hợp lệ vẫn không được phép đọc room của tenant khác, sửa document không thuộc quyền hoặc gọi event admin.
Khó khăn của JWT trên browser WebSocket
Browser API không cho JavaScript tự đặt header Authorization trong WebSocket handshake:
Vì vậy có bốn pattern thường gặp.
1. Secure HttpOnly Cookie
Browser tự gửi cookie trong handshake. Ưu điểm là token không lộ cho JavaScript. Tuy nhiên phải:
Dùng Secure, HttpOnly, SameSite phù hợp.
Validate Origin nghiêm ngặt để chống Cross-Site WebSocket Hijacking.
Kiểm soát CSRF/session fixation và logout.
Không coi Origin là Authentication.
2. WebSocket ticket sống ngắn, dùng một lần
Client dùng access token qua HTTPS để xin một ticket riêng:
Server trả JWT/ticket với:
aud = websocket
sub = userId
exp rất ngắn
jti duy nhất
scope/document/tenant cần thiết
Client kết nối:
Query string có thể xuất hiện trong proxy log, vì vậy ticket phải ngắn hạn, single-use và log phải redact toàn bộ query. Không đặt access token chính sống lâu trong URL.
3. First-message authentication
Client Upgrade trước rồi gửi { type: "auth", token }. Cách này đơn giản nhưng unauthenticated socket đã chiếm tài nguyên. Cần auth deadline rất ngắn, giới hạn pre-auth connection và không cho gọi event khác trước khi xác thực.
4. Sec-WebSocket-Protocol
Một số hệ thống truyền credential qua danh sách subprotocol. Cách này cần cẩn thận vì header có thể bị log, proxy có thể xử lý khác nhau và server phải trả đúng subprotocol đã chọn. Không dùng nếu chỉ đang “nhét token vào chỗ trống” mà không hiểu semantics.
Luồng đề xuất
Ticket tách credential dài hạn khỏi URL và cho phép giới hạn scope riêng cho WebSocket.
Verify JWT đúng cách với jose
Các nguyên tắc:
Verify signature, không chỉ decode payload.
Khóa thuật toán được chấp nhận; không tin alg tùy ý từ token.
Kiểm tra iss, aud, exp, nbf và claims bắt buộc.
Cache JWKS có kiểm soát và hỗ trợ key rotation.
Phân biệt access token, ID token và WebSocket ticket bằng audience/type.
Không log token hoặc toàn bộ payload.
Không dùng secret yếu cho HMAC.
Upgrade handler an toàn với Node.js và ws
Ví dụ bên dưới xác thực trước khi handleUpgrade, nên socket chưa hợp lệ không đi vào WebSocket server.
getTrustedClientIp() chỉ được đọc CF-Connecting-IP hoặc X-Forwarded-For khi request đến từ reverse proxy đã tin cậy. Nếu origin public và tin header tùy ý, attacker có thể giả IP để vượt rate limit.
Quản lý lifecycle của identity
Sau khi connection thành công:
Nếu ticket chỉ dùng để handshake, không nên đóng connection theo thời gian sống cực ngắn của ticket. Khi đó ticket cần mang sessionExpiresAt riêng, hoặc server load session policy sau khi consume ticket. Phải định nghĩa rõ token nào kiểm soát connect deadline và token nào kiểm soát session lifetime.
Với connection nhiều giờ, chọn một trong các policy:
Đóng khi access/session hết hạn và buộc reconnect.
Cho phép in-band reauthentication bằng token mới.
Dùng session version/revocation list để logout có hiệu lực.
Re-check quyền trước action nhạy cảm.
Authorization trên từng message
Message envelope nên có schema rõ ràng:
Không cho client tự chọn tenantId rồi tin giá trị đó. Tenant phải đến từ identity/session và resource lookup phải bảo đảm cùng tenant.
Cache authorization giúp giảm database load, nhưng TTL phải ngắn hoặc có invalidation khi quyền thay đổi.
Schema validation và input security
Mọi message đều là input không tin cậy:
Chỉ cho phép event type trong allowlist.
Giới hạn độ dài string, số lượng field, array size và nesting depth.
Reject field thừa khi schema yêu cầu nghiêm ngặt.
Dùng parameterized query.
Escape/sanitize khi render HTML.
Không merge object không kiểm soát vào config hoặc prototype.
Giới hạn room joins, subscriptions và request đồng thời.
Dùng request timeout và cancellation cho downstream work.
maxPayload bảo vệ kích thước WebSocket message nhưng không thay thế schema validation.
Chống DDoS và Connection Flooding theo nhiều tầng
Một app server không thể tự chống volumetric DDoS làm nghẽn đường truyền. Cần upstream provider/CDN/DDoS protection.
Nginx giới hạn handshake và concurrent connections
Các con số chỉ là ví dụ. NAT doanh nghiệp hoặc carrier-grade NAT có thể đặt nhiều user sau cùng IP. Roll out bằng dry-run/monitoring và kết hợp key theo IP, user, tenant và reputation.
limit_req ở location WebSocket chủ yếu giới hạn handshake HTTP, không giới hạn frame sau khi Upgrade. Message flood phải xử lý trong ứng dụng.
Token Bucket cho message rate
Không phải event nào cũng có cost bằng nhau:
Áp dụng quota theo:
Connection.
User/account.
IP/network.
Tenant.
Resource/room.
Event type.
Toàn cluster.
Limiter in-memory chỉ bảo vệ một node. Với nhiều node, quota user/tenant quan trọng cần Redis hoặc distributed rate limiter có atomic operation.
Connection limits và pre-auth timeout
Đặt ngân sách rõ ràng:
Handshakes/giây/IP.
Concurrent pre-auth sockets/IP.
Concurrent authenticated sockets/user.
Connections/tenant.
Global maximum trên node.
Thời gian hoàn tất authentication.
Room/subscription tối đa/socket.
Khi node gần cạn file descriptor hoặc memory, phải load shed sớm thay vì nhận thêm rồi crash toàn bộ.
Heartbeat và zombie cleanup
Connection flood không chỉ đến từ attacker; socket zombie cũng giữ tài nguyên. Server cần Ping/Pong và terminate client không phản hồi. Presence/room cleanup phải chạy cho close, error, heartbeat timeout và process shutdown.
Không đặt timeout vô hạn chỉ để tránh disconnect. Heartbeat interval phải nhỏ hơn idle timeout của Nginx/Cloudflare nhưng không quá dày gây amplification.
Backpressure và outbound amplification
Một message nhỏ có thể trigger broadcast tới hàng nghìn client. Authorization không ngăn amplification.
Cần:
Giới hạn fan-out và room size cho từng action.
Không cho user thường broadcast global.
Theo dõi bufferedAmount.
Coalesce presence/cursor updates.
Đóng slow consumers vượt buffer budget.
Không nén payload nhỏ mặc định; benchmark compression riêng.
Giới hạn response size và pagination.
DDoS nội bộ thường đến từ action hợp lệ nhưng quá đắt.
Origin validation và CSWSH
Browser tự gửi cookie trong WebSocket handshake, nên website độc hại có thể cố mở socket bằng credential của nạn nhân. Validate Origin bằng exact allowlist:
Không dùng:
Cách đó có thể chấp nhận evil-example.com. Không phản chiếu Origin tùy ý.
Non-browser client có thể không gửi hoặc giả Origin, vì vậy Origin là CSWSH control chứ không phải identity. API cho machine client cần credential riêng, mTLS hoặc policy rõ ràng.
Logging và audit mà không rò rỉ secret
Nên log:
Handshake accepted/rejected và reason code.
User/tenant ID sau xác thực.
Origin và trusted client IP.
Authorization denial.
Rate-limit trigger và limiter scope.
Message type, kích thước, latency và result.
Close code/reason, lifetime và heartbeat timeout.
Không log:
JWT, cookie, query ticket.
Full payload có dữ liệu nhạy cảm.
Password, API key hoặc message riêng tư.
Cấu hình access log bỏ query string ở đường /ws, hoặc redact tham số ticket trước khi gửi log.
Token revocation và thay đổi quyền
JWT stateless không tự bị vô hiệu hóa sau logout. Các lựa chọn:
Access token ngắn hạn.
sessionVersion trong database/Redis.
Denylist theo jti đến khi hết hạn.
Publish sự kiện session:revoked tới WebSocket nodes.
Close toàn bộ socket của user khi password đổi hoặc account bị khóa.
Version ACL/resource và invalidate authorization cache.
Với action nhạy cảm, kiểm tra trạng thái session hiện tại thay vì chỉ tin claims từ lúc connection mở.
Security testing
Kịch bản bắt buộc:
Kết nối không token, token giả, token hết hạn, sai issuer/audience.
Reuse ticket single-use.
Kết nối từ Origin không được phép.
User A đọc/sửa resource của User B hoặc tenant khác.
Fuzz WebSocket messages bằng OWASP ZAP hoặc công cụ tương đương trong môi trường được phép.
Chỉ thực hiện load/security test trên hệ thống được ủy quyền và có giới hạn an toàn.
Metrics và cảnh báo
Theo dõi:
Handshake rate và rejection theo reason.
Concurrent pre-auth/authenticated connections.
Connections theo node, tenant và IP bucket.
JWT verification latency/error.
Authorization denials.
Messages/second và bytes/second theo event type.
Rate-limit hits.
Oversized/invalid message count.
Outbound buffered bytes và slow consumers.
CPU, RSS, event-loop lag, file descriptors và Redis latency.
Cảnh báo khi handshake tăng đột biến, tỷ lệ auth failure cao, một event type chiếm CPU bất thường hoặc số connection tăng nhưng user active không tăng tương ứng.
Anti-patterns
Đặt access token dài hạn trong WebSocket URL.
Chỉ decode JWT mà không verify chữ ký.
Không khóa algorithm/issuer/audience.
Xác thực một lần rồi bỏ qua authorization.
Tin userId hoặc tenantId từ message.
Dùng Origin như Authentication.
Chỉ rate limit ở Nginx rồi bỏ qua message flood.
Limiter chỉ theo IP và làm hỏng user sau NAT.
Không giới hạn payload, room hoặc outbound buffer.
Nén mọi message mà không benchmark CPU/memory.
Log query string chứa ticket.
Không revoke socket khi session bị vô hiệu hóa.
Checklist production
Chỉ dùng wss:// trong production.
Browser dùng HttpOnly cookie hoặc ticket ngắn hạn/single-use.
JWT được verify signature, issuer, audience, expiry và algorithm.
Origin được kiểm tra bằng exact allowlist.
Ticket/query/token được redact khỏi log.
Authentication xảy ra trước Upgrade hoặc có pre-auth timeout nghiêm ngặt.
Authorization chạy trên từng action/resource.
Message có schema allowlist và size/depth limits.
Có handshake rate limit và concurrent connection limit.
Có per-message/user/tenant/distributed quota.
Heartbeat thu hồi zombie connections.
Outbound buffer và fan-out có budget.
Session revocation và token expiry có policy.
DDoS volumetric được chặn ở edge/upstream.
Metrics, security logs và alert đã hoạt động.
Đã test CSWSH, horizontal authorization, floods và reconnect storm.
Kết luận
Bảo mật WebSocket production cần ba vòng kiểm soát:
Handshake: TLS, Origin, rate limit, JWT/session verification và connection quota.
Message: schema validation, authorization, event cost và resource limits.
Lifecycle: token expiry/revocation, heartbeat, backpressure, cleanup và observability.
JWT chỉ chứng minh identity và claims tại một thời điểm. Nó không thay thế Authorization, không chống DDoS và không giới hạn tài nguyên. Hệ thống an toàn cần kết hợp edge protection, reverse proxy, gateway và business service.