이 글에서는 실시간 양방향 통신을 가능하게 하는 WebSocket의 보안 취약점과 이를 방어하기 위한 핵심적인 방법론을 다룹니다. WebSocket은 HTTP와 달리 한번 연결이 수립되면 계속해서 데이터를 주고받을 수 있어 효율적이지만, 이 특성 때문에 새로운 보안 위협에 노출될 수 있습니다.
Core Principles of WebSocket Security
WebSocket 보안을 강화하기 위해선 다음의 핵심 원칙들을 반드시 고려해야 합니다. 각 항목은 잠재적인 공격 벡터를 차단하는 데 중요한 역할을 합니다.
원칙 | 설명 |
---|---|
Encrypt Communication (WSS) | 항상 wss:// 프로토콜을 사용하여 통신을 암호화해야 합니다. ws:// 는 평문으로 데이터를 전송하여 스니핑 공격에 취약하지만, wss:// 는 TLS(Transport Layer Security)를 통해 데이터를 암호화하여 중간자(MITM) 공격으로부터 통신을 보호합니다. |
Validate Origin Headers | Cross-Site WebSocket Hijacking (CSWSH) 공격을 방어하기 위해 서버는 WebSocket 핸드셰이크 요청 시 Origin 헤더를 반드시 검증해야 합니다. 허용된 도메인 목록(Allowlist)을 만들어, 해당 목록에 없는 출처로부터의 연결 요청은 거부해야 합니다. |
Implement Robust Authentication | WebSocket 프로토콜 자체에는 인증 메커니즘이 없습니다. 따라서 연결 초기 단계인 HTTP 핸드셰이크 과정에서 사용자를 인증해야 합니다. 쿠키, 세션, JWT(JSON Web Token) 등 검증된 방식을 활용할 수 있습니다. |
Sanitize and Validate All Inputs | 클라이언트로부터 수신하는 모든 데이터는 신뢰할 수 없는 것으로 간주해야 합니다. XSS(Cross-Site Scripting), SQL Injection 등과 같은 Injection 공격을 막기 위해 서버 측에서 모든 메시지에 대해 강력한 유효성 검사 및 데이터 새니타이제이션(Sanitization)을 수행해야 합니다. |
Common Vulnerabilities and Mitigations
WebSocket 환경에서 자주 발견되는 주요 취약점과 그에 대한 대응 방안은 다음과 같습니다.
Cross-Site WebSocket Hijacking (CSWSH)
CSWSH는 공격자가 사용자의 브라우저를 이용해 비인가 WebSocket 연결을 수립하도록 유도하는 공격입니다. 사용자가 인증된 세션(예: 쿠키)을 가지고 있을 경우, 공격자의 악성 페이지는 사용자의 권한으로 서버와 통신할 수 있게 됩니다.
- Mitigation: 앞서 언급했듯, 서버는 핸드셰이크 요청의
Origin
헤더를 엄격하게 검증하여 허용된 출처에서 온 요청만 수락해야 합니다.
// Example: Origin header validation in Node.js (ws library)
const allowedOrigins = ['https://hahwul.com', 'https://sub.hahwul.com'];
wsServer.on('request', (request) => {
const origin = request.origin;
if (!allowedOrigins.includes(origin)) {
// Reject the connection
request.reject();
console.log(`Connection from origin ${origin} rejected.`);
return;
}
// ... accept connection
});
Data Injection Attacks (XSS, SQLi)
WebSocket을 통해 전달되는 데이터가 적절히 처리되지 않으면 서버나 다른 클라이언트에게 악의적인 영향을 미칠 수 있습니다. 예를 들어, 채팅 애플리케이션에서 한 사용자가 보낸 스크립트가 다른 사용자들의 브라우저에서 실행되면 XSS 공격이 성립됩니다.
- Mitigation: 서버는 클라이언트로부터 받은 모든 메시지의 내용을 검증하고, 데이터베이스 쿼리나 HTML 출력에 사용하기 전에 반드시 이스케이프(Escape) 또는 새니타이즈(Sanitize) 해야 합니다. 신뢰할 수 없는 데이터를 다룰 때는 항상 주의를 기울여야 합니다.
Denial-of-Service (DoS)
공격자는 대량의 연결을 생성하거나, 처리 비용이 높은 메시지를 대량으로 전송하여 서버의 리소스를 고갈시킬 수 있습니다. 이는 정상적인 사용자의 서비스 이용을 방해합니다.
- Mitigation:
- Rate Limiting: 특정 IP 주소나 사용자 계정당 연결 수 및 메시지 전송률을 제한합니다.
- Message Size Limits: 비정상적으로 큰 크기의 메시지를 차단하여 버퍼 오버플로우나 리소스 낭비를 방지합니다.
Authentication and Authorization
WebSocket은 상태를 유지하는(Stateful) 프로토콜이지만, 인증은 상태가 없는(Stateless) HTTP 핸드셰이크를 통해 이루어져야 합니다.
sequenceDiagram participant Client participant Server Client->>Server: HTTP GET /chat (Upgrade: websocket) Note over Server: 1. Verify Origin Header Note over Server: 2. Authenticate User (e.g., via Cookie/JWT) alt Authentication/Origin Check Fails Server-->>Client: HTTP 401/403 Unauthorized else Authentication & Origin OK Server-->>Client: HTTP 101 Switching Protocols Note over Client,Server: WebSocket Connection Established Client-->>Server: WebSocket Message (JSON) Note over Server: 3. Validate & Sanitize Message Server-->>Client: WebSocket Message (Broadcast) end
일반적으로 사용되는 인증 방식은 다음과 같습니다.
- Cookie-based Authentication: 웹 애플리케이션과 동일한 도메인에서 WebSocket을 운영하는 경우, 브라우저는 자동으로 인증 쿠키를 전송합니다. 서버는 이 쿠키를 검증하여 사용자를 식별할 수 있습니다.
- Token-based Authentication: 클라이언트는 로그인 시 발급받은 Token(예: JWT)을 쿼리 파라미터나 첫 번째 WebSocket 메시지를 통해 서버로 전송합니다. 서버는 이 Token의 유효성을 검증한 후 연결을 허용합니다.
// Token via query parameter
const socket = new WebSocket('wss://api.hahwul.com/ws?token=YOUR_JWT_TOKEN');
// Token as the first message
socket.onopen = () => {
socket.send(JSON.stringify({ type: 'auth', token: 'YOUR_JWT_TOKEN' }));
};
Token을 쿼리 파라미터로 전송하는 방식은 구현이 간단하지만, 서버 로그에 Token이 남을 수 있다는 단점이 있습니다. 반면, 연결 후 첫 메시지로 전송하는 방식은 더 안전하지만, 인증 전까지 다른 메시지를 처리하지 않도록 로직을 구현해야 합니다.
Conclusion
WebSocket은 현대적인 웹 애플리케이션의 필수 요소로 자리 잡았지만, 그 편리함 이면에는 반드시 고려해야 할 보안 문제들이 존재합니다. 이 글에서 다룬 wss://
사용 의무화, Origin
헤더 검증, 입력 데이터 처리, 그리고 견고한 인증 메커니즘 적용은 안전한 WebSocket 환경을 구축하기 위한 최소한의 요구사항입니다. Red team 관점에서 볼 때, 이러한 기본 방어책이 없는 시스템은 쉽게 공격의 표적이 될 수 있습니다. 따라서 개발 초기 단계부터 보안을 염두에 두고 설계하는 것이 매우 중요합니다.