What is Server-Sent Events (SSE)?

Là một loại công nghệ cho phép server gửi các events đến cho client. Khi browser mở kết nối đến server, kết nối đó sẽ được duy trì (giống WebSocket) nhưng data chỉ truyền một chiều từ server đến client, cho đến khi nào client hoặc server đóng connection.

SSE hoạt động trên HTTP và mục đích chính thường là streaming data từ server đến client.

How it Works

Quá trình trao đổi data sử dụng SSE hoạt động như sau:

  1. Client Connection: client tạo ra một EventSource để kết nối đến một endpoint cụ thể.
  2. Server Response: server phản hồi lại request này bằng cách trả về response có Content-Typetext/event-stream.
  3. Persistent Connection: server duy trì connection liên tục.
  4. Data Push: khi có data mới hoặc có event xảy ra, server sẽ gửi messages đến cho client theo một định dạng cụ thể.

Minh họa:

Event Stream Format

Data gửi đến client có định dạng key: value, phân cách nhau bởi 2 ký tự xuống dòng \n\n. Một số key chính:

  • data: chứa dữ liệu được gửi cho client
  • event: specify loại event mà client có thể lắng nghe. Nếu không dùng key này thì event type mặc định là message (giống với postMessage).
  • id: là ID độc nhất của event. Khi client tái kết nối lại với server, nó có thể gửi id của event cuối cùng nhận được trong Last-Event-ID header để khôi phục các data bị mất.
  • retry: chỉ định thời gian (đơn vị milliseconds) mà client nên chờ trước khi thực hiện tái kết nối.

Một số ví dụ:

// Simple message
data: This is a message.
 
// JSON data with a custom event type
event: user_update
data: {"username": "hahwul", "status": "online"}
 
// Message with an ID for synchronization
id: msg1
data: Some data stream

SSE vs. WebSockets vs. Polling

Bảng so sánh giữa 3 loại công nghệ mà đều có điểm chung là stream dữ liệu real-time:

FeaturePollingLong-PollingSSE (Server-Sent Events)WebSockets
DirectionClient ServerClient ServerServer Client (Unidirectional)Bidirectional
ProtocolHTTPHTTPHTTPWebSocket (ws://, wss://)
ConnectionNew connection per requestLong-lived, then newSingle persistent connectionSingle persistent connection
OverheadHighMediumLowLow (after handshake)
Use CaseInfrequent updatesDelayed updatesNotifications, Live FeedsChat, Gaming, Collaboration
ReconnectionManualManualAutomatic (built-in)Manual

Client-Side Implementation

Ví dụ implement client sử dụng EventSource API:

const eventSource = new EventSource("/stream")
 
// General message listener
eventSource.onmessage = (event) => {
  console.log("New message:", event.data)
}
 
// Listener for custom events
eventSource.addEventListener("notification", (event) => {
  const notificationData = JSON.parse(event.data)
  console.log("Notification:", notificationData.message)
})
 
// Error handling
eventSource.onerror = (err) => {
  console.error("EventSource failed:", err)
  // EventSource will automatically try to reconnect.
  // To close it permanently:
  // eventSource.close();
}

How to Secure SSE

Authentication and Authorization

EventSource mặc định không hỗ trợ custom HTTP header chẳng hạn như Authorization nên việc dùng token để xác thực có thể bị giới hạn và thường có 2 cách thay thế là:

  • Cookie: mặc định sẽ được sử dụng bởi EventSource API.
  • Token ở query param: rủi ro do nó có thể bị lưu trong browser history, server logs, etc.

CSRF

Các SSE request thường có method là GET nên chúng có thể bị tấn công CSRF nhằm dụ dỗ người dùng thiết lập kết nối SSE và đánh cắp dữ liệu real-time.

  • Origin Header Check: kiểm tra Origin header của request để chỉ cho phép đọc response từ các domains được ủy quyền.
  • SameSite attribute: sử dụng Lax hoặc Strict để cookie không bị gửi bởi các request từ các origins khác1.

XSS

Nếu data mà server gửi về cho client được render trực tiếp vào DOM thì có thể bị tấn công XSS. Ví dụ:

const outputDiv = document.getElementById("output")
 
eventSource.onmessage = (event) => {
  // Vulnerable to XSS: Never do this!
  // outputDiv.innerHTML += event.data + '<br>';
 
  // Safe: Use textContent to render data as plain text.
  const p = document.createElement("p")
  p.textContent = event.data
  outputDiv.appendChild(p)
}

Denial of Service (DoS)

Do các SSE connection thường được duy trì rất lâu nên chúng có thể là mục tiêu của tấn công DoS. Attacker có thể tạo ra nhiều connection đồng thời để làm cạn kiệt tài nguyên của server. Do đó, ta nên:

  • Triển khai rate limiting
  • Chỉ cho phép sử dụng một lượng nhất định các connection đồng thời.

Resources

Footnotes

  1. Xem thêm Bypassing SameSite Cookie Restrictions