SSE Security

Server Sent Event

๐Ÿ” 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