브라우저 JavaScript는 왜 UDP를 직접 사용할 수 없을까?
네트워크 공부를 하다가 문득 UDP API를 브라우저에서 사용할 수 없는지 궁금해졌다.
먼저 전제를 정확히 하면, JavaScript 언어 자체가 UDP를 지원하지 않는 것이 아니다. 일반 웹페이지에서 실행되는 JavaScript에 브라우저가 범용 UDP 소켓 API를 제공하지 않는 것이다.
Node.js처럼 운영체제 프로세스로 실행되는 JavaScript 환경에서는 UDP를 사용할 수 있다. 반면 일반 웹페이지는 인터넷에서 내려받은 코드를 사용자가 페이지에 접속하는 것만으로 실행할 수 있기 때문에 훨씬 강한 보안 제약을 받는다.
일부 설치형·격리형 웹 애플리케이션 환경에는 별도의 권한 모델을 전제로 소켓 기능이 제공되기도 하지만, 우리가 일반적으로 Chrome이나 Safari에서 여는 웹페이지에는 임의의 IP 주소와 UDP 포트로 데이터그램을 보내는 API가 제공되지 않는다.
꼬리에 꼬리를 물어 이유를 따라가 보자.
1. 일반 웹페이지에는 왜 UDP 소켓 API가 없을까?
→ 웹페이지에 임의의 네트워크 엔드포인트와 직접 통신하는 권한을 주지 않기 위해서다.
일반 웹페이지의 JavaScript는 개발자가 인터넷을 통해 전달한 코드다.
사용자가 프로그램을 별도로 설치하거나 실행 파일을 직접 실행하지 않아도 페이지를 여는 순간 코드가 동작한다.
사용자
↓
웹사이트 접속
↓
웹사이트에서 JavaScript 다운로드
↓
브라우저가 JavaScript 실행이 환경에서 웹페이지에 다음과 같은 API가 있다고 가정해 보자.
UDP 소켓 생성
목적지 IP 지정
목적지 포트 지정
원하는 바이트 데이터 지정
전송그러면 웹페이지는 HTTP나 WebSocket처럼 브라우저가 정한 애플리케이션 프로토콜에 제한되지 않고 임의의 네트워크 서비스와 직접 통신할 수 있게 된다.
중요한 점은 이 문제가 UDP에만 해당하지 않는다는 것이다.
일반 웹페이지에 임의의 TCP 서버와 연결할 수 있는 범용 TCP 소켓 API를 제공해도 비슷한 문제가 발생한다.
즉 핵심은
UDP가 위험해서 차단이 아니라
신뢰할 수 없는 웹페이지
+
임의 IP·포트에 직접 연결하는 범용 소켓 권한
=
웹 보안 모델과 충돌이다.
2. 왜 임의의 IP와 포트로 통신하는 것이 문제일까?
→ 웹사이트가 사용자의 컴퓨터를 통해 인터넷뿐 아니라 사용자의 내부 네트워크에도 접근을 시도할 수 있기 때문이다.
예를 들어 사용자의 집에 다음 장비가 있다고 하자.
192.168.0.1 공유기
192.168.0.20 프린터
192.168.0.30 CCTV
192.168.0.40 IoT 장치이 장비들은 인터넷에서 직접 접근할 수 없는 경우가 많다.
하지만 사용자의 브라우저는 이미 그 내부 네트워크 안에서 실행되고 있다.
인터넷
│
▼
사용자의 공유기
│
├── PC ── 브라우저
│
├── 프린터
│
├── CCTV
│
└── IoT 장치만약 웹페이지가 자유롭게 UDP 소켓을 만들 수 있다면 상황이 달라진다.
악성 웹사이트
↓
사용자 브라우저에서 JavaScript 실행
↓
사용자의 네트워크 권한을 이용
↓
192.168.0.x 장비들에 패킷 전송사용자는 단순히 인터넷 웹페이지 하나를 열었을 뿐인데 그 페이지의 코드가 내부 네트워크 장비와 직접 통신을 시도하게 된다.
3. UDP의 어떤 특징이 이 문제와 관련될까?
→ UDP는 연결 설정 과정 없이 데이터그램 단위로 데이터를 전송하는 프로토콜이다.
TCP에서는 일반적으로 데이터를 교환하기 전에 연결을 먼저 설정한다.
TCP
클라이언트
│
│ 연결 설정
▼
서버
│
│ 연결 성립
▼
데이터 전송UDP에는 TCP와 같은 연결 설정 과정이 없다.
애플리케이션은 목적지 IP 주소와 포트를 지정하고 데이터그램을 전송한다.
UDP
데이터 준비
↓
목적지 IP 지정
↓
목적지 UDP 포트 지정
↓
데이터그램 전송하지만 여기서 주의해야 한다.
브라우저가 UDP를 제한하는 근본 원인이 “UDP에는 핸드셰이크가 없기 때문”인 것은 아니다.
핵심 문제는 웹페이지에
임의 IP
+
임의 포트
+
임의 데이터를 지정해 통신하는 범용 네트워크 권한을 제공하는 것이다.
UDP의 비연결형 특성은 이러한 통신을 단순하게 만들지만, 보안 문제의 근본은 소켓 권한 자체에 있다.
4. 브라우저에 UDP API가 있다고 가정하면 어떻게 될까?
다음 코드는 브라우저에 UDP API가 존재한다고 가정한 가상의 코드다.
실제 일반 브라우저에는 아래와 같은 navigator.udp API가 존재하지 않는다.
<script>
// 존재하지 않는 가상의 브라우저 UDP API
async function sendUdpPacket() {
const socket = await navigator.udp.open();
const message = new TextEncoder().encode(
"임의의 데이터"
);
await socket.send({
address: "192.168.0.30",
port: 9999,
data: message
});
socket.close();
}
// 사용자가 페이지를 열면 실행
sendUdpPacket();
</script>이런 API가 일반 웹페이지에 제공된다면 사용자는 다음 과정에 관여하지 않는다.
사용자
↓
웹페이지 접속
↓
JavaScript 자동 실행
↓
UDP 소켓 생성
↓
192.168.0.30:9999로 데이터그램 전송
↓
내부 장치가 패킷 수신단순히 "임의의 데이터"를 보냈다고 해서 장비가 즉시 조작되는 것은 아니다.
네트워크 장비는 자신이 사용하는 프로토콜의 형식에 맞는 데이터를 받아야 의미 있는 명령으로 처리한다.
예를 들어 어떤 장치가 다음과 같은 자체 프로토콜을 사용한다고 가정하자.
[명령 코드][장치 ID][설정 값][검증 값]공격자가 이 형식을 모른다면 임의의 문자열을 보내도 장비가 무시할 가능성이 높다.
따라서 위험은
패킷 하나 전송
→ 즉시 장비 장악이 아니다.
실제 문제는 웹사이트가 이런 네트워크 접근 자체를 사용자 허락 없이 반복적으로 시도할 수 있게 된다는 점이다.
5. 내부 네트워크 장비를 탐색하는 것도 가능한가?
→ 범용 UDP 소켓이 있다면 여러 내부 주소와 서비스에 요청을 보내고 특정 응답을 관찰해 네트워크 정보를 추정하는 시도가 가능해진다.
예를 들어 공격자가 특정 프로토콜에 맞는 데이터를 여러 주소에 보낸다고 가정하자.
192.168.0.1 → 특정 응답 수신
192.168.0.20 → 응답 없음
192.168.0.30 → 예상한 형식의 응답 수신이를 통해 특정 주소에 어떤 종류의 서비스가 존재하는지 추정할 수 있다.
다만 UDP에서는 다음 관계가 성립하지 않는다.
응답 없음 = 장비 없음응답이 없는 이유는 여러 가지다.
장비가 존재하지 않을 수도 있고, 방화벽이 패킷을 버렸을 수도 있고, 서비스가 해당 메시지를 무시했을 수도 있으며, 공격자가 보낸 데이터의 프로토콜 형식이 틀렸을 수도 있다.
따라서 UDP 탐색은 단순히 “응답 여부만 확인하면 장비를 모두 찾는다”는 구조는 아니다.
더 정확하게 표현하면 다음과 같다.
특정 주소·포트에
프로토콜에 맞는 데이터 전송
↓
예상한 응답이 오는지 확인
↓
해당 서비스의 존재 가능성 추정그럼에도 일반 웹사이트에 이런 네트워크 탐색 수단을 제공하는 것 자체가 보안상 문제가 된다.
6. 장비의 존재를 알아내는 것만으로도 왜 문제일까?
→ 내부 네트워크의 공격 표면을 파악하는 정보가 되기 때문이다.
공격자가 다음 정보를 알아냈다고 하자.
192.168.0.1 → 특정 공유기 서비스
192.168.0.30 → 특정 CCTV 프로토콜
192.168.0.40 → 특정 IoT 장치그다음에는 각 서비스가 사용하는 프로토콜을 분석할 수 있다.
만약 해당 장비가 인증 없이 명령을 받아들이거나, 취약한 펌웨어를 사용하거나, 비정상적인 입력을 제대로 검증하지 않는다면 공격 가능성이 생긴다.
가능한 공격 범주는 다음과 같다.
- 내부 네트워크 구성 정보 수집
- 취약한 IoT 장치 또는 관리 서비스 접근
- 장비가 사용하는 프로토콜을 이용한 비정상 명령 전송
- 취약한 서비스에 비정상적인 입력 전달
- 내부에서만 접근 가능하다고 가정한 서비스에 대한 공격
여기서 중요한 보안 문제가 하나 더 생긴다.
일부 내부 장비는 과거부터 다음과 같은 가정을 전제로 설계되어 왔다.
내부 네트워크에서 접속했다
↓
어느 정도 신뢰할 수 있는 사용자다하지만 브라우저에서 실행되는 인터넷 웹사이트가 내부망에 자유롭게 접근할 수 있다면 이 가정이 깨진다.
인터넷의 공격자
↓
악성 웹페이지
↓
사용자의 브라우저
↓
사용자의 내부 네트워크브라우저가 일종의 중간 지점으로 이용되는 것이다.
7. 외부 인터넷 서버에 UDP를 보내는 것은 어떤 문제가 있을까?
→ 많은 방문자의 브라우저를 대량 트래픽 발생 수단으로 악용할 가능성이 생긴다.
어떤 사이트에 동시에 많은 사용자가 접속한다고 가정하자.
사용자 A 브라우저 ─┐
사용자 B 브라우저 ─┤
사용자 C 브라우저 ─┼──▶ 특정 서버
사용자 D 브라우저 ─┤
사용자 E 브라우저 ─┘각 브라우저에서 JavaScript가 자동으로 UDP 데이터를 반복 전송할 수 있다면 웹사이트 운영자는 방문자들의 네트워크를 자신이 원하는 트래픽 생성에 이용할 수 있게 된다.
이 역시 UDP만의 문제는 아니다.
범용 TCP 소켓을 unrestricted하게 제공해도 여러 사용자의 브라우저를 특정 네트워크 서비스에 연결시키는 문제가 발생한다.
UDP에서는 연결 설정 없이 데이터그램을 전송하는 특성 때문에 이런 형태의 트래픽 생성이 특히 단순해진다는 차이가 있다.
8. 그런데 fetch도 다른 서버에 데이터를 보내는데 왜 허용될까?
→ fetch는 웹페이지에 운영체제의 범용 네트워크 소켓을 직접 제공하는 API가 아니기 때문이다.
JavaScript에서 다음 코드를 실행한다고 하자.
fetch("https://example.com/api", {
method: "POST",
body: JSON.stringify({
message: "hello"
})
});JavaScript가 직접 하는 것은 대략 다음 수준이다.
이 URL에
POST 요청을 보내고
이 데이터를 body로 사용해 달라그 아래의 실제 네트워크 통신은 브라우저가 담당한다.
웹페이지 JavaScript
│
│ fetch()
▼
브라우저
│
├─ HTTP 규칙 적용
├─ 허용되지 않는 헤더 제어
├─ TLS/HTTPS 처리
├─ 인증서 검증
├─ Same-Origin/CORS 정책 적용
├─ 콘텐츠 보안 정책 적용
└─ 로컬 네트워크 관련 접근 제한 적용
│
▼
HTTP 서버따라서 JavaScript는 다음과 같은 일을 하지 않는다.
TCP socket 직접 생성
→ 임의 포트에 연결
→ 원하는 바이트열 직접 전송브라우저가 HTTP라는 정해진 프로토콜의 중간자로 동작하는 것이다.
여기서 CORS를 정확히 이해해야 한다.
CORS는
다른 출처로는 어떤 요청도 보내지 못하게 한다라는 시스템이 아니다.
일부 교차 출처 요청은 실제 네트워크로 전송될 수 있다.
대신 브라우저는 요청 종류에 따라 사전 요청인 preflight를 수행하거나, 서버의 CORS 응답 헤더를 확인한 뒤 JavaScript가 응답 내용을 읽도록 허용할지 결정한다.
따라서 다음 설명은 정확하지 않다.
fetch가 안전한 이유
→ CORS가 다른 서버로 패킷을 못 보내게 해서더 정확한 설명은 다음과 같다.
fetch가 범용 socket과 다른 이유
→ 브라우저가 HTTP 통신을 직접 중개하고
→ JavaScript가 사용할 수 있는 프로토콜과 요청 형태를 제한하며
→ 출처 정책과 여러 네트워크 보안 정책을 추가로 적용하기 때문9. WebSocket은 양방향 통신이 가능한데 왜 허용될까?
→ WebSocket 역시 웹페이지에 범용 TCP 소켓을 제공하는 API가 아니기 때문이다.
WebSocket 연결은 먼저 WebSocket 프로토콜에 맞는 opening handshake를 수행한다.
브라우저
│
│ WebSocket opening handshake
▼
WebSocket 서버
│
│ 프로토콜에 맞는 응답
▼
WebSocket 연결 성립따라서 브라우저 JavaScript가 다음과 같이 행동하는 것은 불가능하다.
TCP socket 생성
→ 192.168.0.20:9100 연결
→ 임의 바이트열 직접 전송대신 JavaScript는 브라우저가 관리하는 WebSocket 연결을 사용한다.
const socket = new WebSocket("wss://example.com/socket");
socket.addEventListener("open", () => {
socket.send("hello");
});실제 네트워크에서는 브라우저가 WebSocket 규격에 맞는 handshake와 frame을 생성한다.
클라이언트에서 서버로 보내는 WebSocket frame에는 masking도 적용된다.
이 masking은 데이터를 암호화하는 기능이 아니다.
목적은 브라우저에서 생성된 WebSocket 트래픽이 중간 프록시나 WebSocket을 이해하지 못하는 다른 프로토콜 서버에 공격자가 원하는 형태의 데이터로 오인되는 위험을 줄이는 데 있다.
즉 WebSocket 역시
JavaScript
→ 임의 TCP 프로토콜이 아니라
JavaScript
→ 브라우저가 강제하는 WebSocket 프로토콜
→ WebSocket을 이해하는 서버라는 구조다.
10. 그렇다면 UDP도 브라우저가 안전하게 제한해서 제공하면 되지 않을까?
→ 가능하다. 실제 웹 기술도 그런 방향으로 UDP의 장점을 제한적으로 사용한다.
문제는 아무 UDP 서버와 자유롭게 통신하는 범용 UDP 소켓 API를 제공하는 것이다.
따라서 브라우저는 범용 UDP 소켓 대신 목적과 프로토콜이 제한된 기술을 제공한다.
WebRTC
WebRTC는 브라우저 간 음성, 영상, 실시간 데이터 통신을 위해 설계되었다.
네트워크 상황에 따라 UDP를 전송 계층으로 사용할 수 있지만 JavaScript가 다음과 같이 직접 지정하는 구조가 아니다.
192.168.0.30:9999에
이 UDP 데이터그램을 보내라WebRTC에서는 연결 상대를 찾는 ICE, 암호화, 미디어 또는 DataChannel 프로토콜 등 별도의 통신 절차가 존재한다.
JavaScript
↓
WebRTC API
↓
연결 협상
↓
ICE / 보안 프로토콜 / 데이터 전송 프로토콜
↓
네트워크WebTransport
WebTransport는 실시간 통신을 위해 stream과 datagram 형태의 전송 기능을 제공한다.
특히 HTTP/3와 QUIC을 사용하는 경우 하위 계층에서 UDP가 사용되지만 JavaScript가 UDP 소켓을 직접 조작하는 것은 아니다.
JavaScript
↓
WebTransport
↓
HTTP/3 / QUIC
↓
UDPJavaScript가 보는 것은 WebTransport 세션과 stream, datagram이지 운영체제 UDP socket이 아니다.
WebSocket
WebSocket 역시 실시간 양방향 통신을 제공하지만 브라우저가 WebSocket 프로토콜을 강제한다.
따라서 세 기술의 공통점은 다음과 같다.
WebRTC
WebTransport
WebSocket
↓
브라우저가 정한 통신 모델과 보안 규칙 안에서 사용반면 범용 UDP 소켓은 다음과 같다.
UDP socket
↓
임의 IP
임의 포트
임의 프로토콜 데이터브라우저가 일반 웹페이지에 제공하지 않는 것은 바로 이 권한이다.
11. 그런데 Node.js에서는 왜 UDP를 사용할 수 있을까?
→ Node.js 코드는 일반 웹페이지의 보안 sandbox 안에서 실행되는 코드가 아니라 운영체제 프로세스로 실행되는 프로그램이기 때문이다.
Node.js에는 UDP 통신을 위한 dgram 모듈이 있다.
개념적으로 다음과 같은 작업이 가능하다.
Node.js 프로세스
↓
UDP socket 생성
↓
목적지 IP·포트 지정
↓
데이터그램 전송브라우저 JavaScript와 Node.js의 차이는 JavaScript라는 언어가 아니다.
코드가 실행되는 보안 환경이 다르다.
일반 웹페이지 JavaScript
인터넷에서 받은 코드
↓
페이지 방문만으로 실행 가능
↓
브라우저 sandbox 안에서 실행
↓
강한 네트워크 권한 제한
↓
범용 TCP/UDP socket 사용 불가반면 Node.js는 다음과 같다.
Node.js 프로그램
운영체제 프로세스로 실행
↓
해당 프로세스의 OS 권한 사용
↓
운영체제 socket API 이용
↓
TCP/UDP 통신 가능Node.js 프로그램이 웹페이지보다 본질적으로 더 안전해서 이런 권한을 받는 것은 아니다.
악성 Node.js 프로그램도 네트워크 공격을 수행할 수 있다.
차이는 운영체제 수준 프로그램의 권한과 책임을 적용받는다는 점이다.
운영체제 방화벽, 사용자 권한, 컨테이너, sandbox 등의 정책이 별도로 Node.js 프로세스를 제한할 수도 있다.
전체 흐름
지금까지의 논리를 한 번에 연결하면 다음과 같다.
일반 웹페이지는
인터넷에서 내려받은 신뢰할 수 없는 코드를 실행한다.
↓
그 코드에 범용 네트워크 socket을 주면
↓
임의의 IP와 포트에 직접 통신할 수 있다.
↓
인터넷 서버뿐 아니라
사용자의 내부 네트워크에도 접근을 시도할 수 있다.
↓
서비스 탐색, 취약한 장비 공격,
비정상적인 데이터 전송,
대량 트래픽 생성 등에 악용될 수 있다.
↓
그래서 일반 웹페이지에는
범용 TCP/UDP socket API를 제공하지 않는다.
↓
대신 fetch, WebSocket, WebRTC, WebTransport처럼
브라우저가 프로토콜과 보안 정책을 통제할 수 있는
고수준 네트워크 API를 제공한다.정리
일반 웹페이지에서 UDP를 직접 사용할 수 없는 근본적인 이유는 UDP에 TCP와 같은 핸드셰이크가 없기 때문이 아니다.핵심은 신뢰할 수 없는 웹페이지에 임의의 IP 주소와 포트로 임의의 프로토콜 데이터를 전송하는 범용 소켓 권한을 제공하는 것이 웹의 보안 모델과 충돌하기 때문이다.
UDP의 비연결형 데이터그램 특성은 이 위험을 더 쉽게 이해하게 해 주는 요소지만, 같은 이유로 일반 웹페이지에는 범용 TCP 소켓도 제공되지 않는다.
브라우저는 대신
fetch, WebSocket, WebRTC, WebTransport처럼 브라우저가 프로토콜과 보안 정책을 중간에서 통제할 수 있는 네트워크 API를 제공한다.반면 Node.js는 브라우저 sandbox가 아니라 운영체제 프로세스로 실행되므로 운영체제가 허용하는 범위에서 TCP와 UDP 소켓을 직접 사용할 수 있다.