SSE Security

Introduction

SSE(Server-Sent Event)는 Server Push 기술로 웹 소캣과 유사하게 서버와 Javascript가 서로 통신하여 데이터를 받아올 수 있습니다. 다만 웹소켓의 경우 양방향 통신이 가능하지만, SSE는 서버→클라이언트로 받는 요청만 처리할 수 있습니다.

단순히 서버로부터 Push를 받아야하는 경우 SSE가 가장 간편한 기술로 사용될 수 있습니다. 다만 성능이나 기술적인 부분에서 이점보단 단점이 많아서 대부분의 서비스에선 WebSocket 또는 Ajax 방식을 많이 사용합니다.

var evtSource = new EventSource

evtSource.onmessage = function(e) {
  var logObject = document.getElementById("logs");
  logObject.innerHTML = "message: " + e.data;
  eventList.appendChild(newElement);
}

Event Straem Format

Basic and content-type

SSE는 text/event-stream 타입과 plain text response를 사용합니다. 그리고 각 line 별로 data: 라는 구분자를 통해서 stream을 전송하고 구별합니다.

data: message\n\n

Multiline

data: first line\n
data: second line\n\n
data: first line\nsecond line\n\n

JSON

data: {\n
data: "user": "hahwul",\n
data: "id": 12345\n
data: }\n\n
data: {\n"user": "hahwul","id: 12345\n}

이 때 SSE를 받는 Client에선 아래와 같이 eventListener로 받은 데이터를 JSON.parse 하여 사용하게 됩니다.

source.addEventListener('message', function(e) {
  var data = JSON.parse(e.data);
  console.log(data.id, data.msg);
}, false);

Event name

event: 구분자를 통해 eventListner로 전달할 이벤트 이름을 명시할 수 있습니다.

event: userlogon\n
data: {"username": "hahwul"}\n\n
event: update\n
data: {"username": "hahwul", "tool": "zap"}\n\n
evtSource.addEventListener('userlogon', function(e) {
  var data = JSON.parse(e.data);
  console.log('login:'+data.username);
}, false);

evtSource.addEventListener('update', function(e) {
  var data = JSON.parse(e.data);
  console.log('update:' + data);
}, false);

Offensive techniques

How to Testing

SSE는 웹 브라우저의 Console, Debugger를 이용해서 데이터에 대해 테스트할 수 있습니다. 기본적으로 SSE는 서버에서 데이터를 받기만 하기 떄문에 EventHandler 이후 로직이 주요 분석 구간이 됩니다.

ZAP에서는 Server-Sent Events Addon 통해서 Recv되는 데이터를 트래킹하고 분석하는데 활용할 수 있습니다.

SSE History in ZAP

Testing Method

SSE는 서버로 부터 데이터를 받기만 가능하기 때문에 공격할 수 있는 범위가 넓지는 않습니다. 다만 SSE에 연결되는 서버가 항상 신뢰된 데이터를 준다는 보장은 없습니다. 그래서 서버로 부터 오는 데이터를 기반으로 DOM에 반영되거나 다른 로직에 처리되는 것을 이용한 공격들이 주를 이루게 됩니다.

DOM XSS

SSE를 통해 받은 데이터에서 DOM XSS가 발생할 수 있습니다. 일반적인 DOM XSS와 동일하며, 데이터를 전달하는 위치가 SSE라는 차이가 있습니다.

evtSource.onmessage = function(e) {
  var logObject = document.getElementById("logs");
  logObject.innerHTML = "message: " + e.data;
  eventList.appendChild(newElement);
}

위와 같은 코드는 서버로 부터 받은 데이터가 logObject의 innerHTML로 들어가기 떄문에 DOM 기반의 XXS가 가능합니다.

data: message<svg/onload=alert(45)>aaaa\n\n

Information Disclosure

SSE 연결 과정에서 별다른 인증 등 별다른 검증 로직이 없는 경우 누구나 연결하여 데이터를 수신할 수 있는 상태가 됩니다. 만약 SSE를 통해 중요한 데이터를 전달한다면 공격자 또한 해당 SSE를 subscribe 하고 정보를 받을 수 있습니다.

var evtSource = new EventSource("https://example.hahwul.com/sse/channel/admin");
evtSource.onmessage = function(e) {
  // ...
  // Leak Admin Data
}

SSE Address Hijacking

만약 동적으로 Client에서 SSE 연결 주소를 설정하는 경우 해당 파라미터 값을 조작하여 공격자가 의도한 사이트로부터 SSE 이벤트를 받도록 유도할 수 있습니다. 이를 활용하면 사용자에게 거짓된 정보를 전달할 수 있게 됩니다.

// GET /blahblah?sse_url=https://poc.attacker.com/sse

var evtSource = new EventSource(getAddressWithParam('sse_url'));
evtSource.onmessage = function(e) {
  // ...
}

Sending Malicious MSG

혹시라도 메시지를 전송하는 기능이 외부 API로 노출되어 있거나 서비스 기능으로 있는 경우 악용될 여지가 높습니다. 혹시라도 외부 API가 열려 있거나 전송하는 기능의 오류로 인해 공격자가 의도한 메시지 전송이 가능할 수 있습니다.

Original Request

POST /notify 

msg=update%20article

SSE Message

data:{"msg":"update article","author":"user1"}

Attack Request

POST /notify 

msg=update%20article","author":"admin","temp":"11

SSE Message

data:{"msg":"update article","author":"admin","temp":"11","author":"user1"}

Etc

이외에도 Browser단 보안 취약점에 동일하게 영향받을 수 있습니다. 다만 소스가 서버라는 차이점만 존재합니다.

Defensive techniques

서버로 부터 오는 데이터를 무조건 신뢰할 수는 없습니다. 만약 SSE로 연결되는 서버가 신뢰 구간에 있는 서버라고 하더라도, 가급적이면 FE단에서 SSE로 인한 이슈는 제한될 수 있도록 별도의 방어로직이 있는 것이 좋습니다.

Prevent XSS

클라이언트에서 SSE로 부터 전달받은 데이터를 DOM에 반영하게 되는 경우 아무리 신뢰 구간에 있는 서버라고 해도 임의로 스크립트가 삽입되지 않도록 XSS에 대한 대응이 필요합니다.

evtSource.onmessage = function(e) {
  var logObject = document.getElementById("logs");
  logObject.innerText = "message: " + e.data;  // innerHTML to innerText
  eventList.appendChild(newElement);
}

Use random address

SSE 연결 과정에서 별다른 인증 등 별다른 검증 로직이 없는 경우 누구나 연결하여 데이터를 수신할 수 있는 상태가 됩니다. 만약 SSE를 통해 중요한 데이터를 전달하게 된다면 외부에서 쉽게 연결할 수 없도록 인증 검사 절차를 추가하거나, EventSource URL을 추측하기 어려운 값의 난수를 이용하여 각 채널들을 분리시켜 임의 접근을 막는게 좋습니다.

var evtSource = new EventSource("https://example.hahwul.com/sse/channel/2413fb3709b05939f04cf2e92f7d0897fc2596f9ad0b8a9ea855c7bfebaae892");
evtSource.onmessage = function(e) {
  // ...
}

Control to Sender

SSE에서 메시지 발송 주체는 서버입니다. 다만 이러한 기능이 API로 공개되어 있거나 비즈니스 로직의 문제로 외부 사용자가 이 기능을 사용할 수도 있습니다. 가급적이면 SSE를 발송하는 기능은 인프라 내부에만 위치 시키는게 좋고 혹여나 외부에 두어야한다면 적절한 접근 통제와 이력 관리를 통해 사고를 예방하는 것이 좋습니다.

Tools

References