COMMENTS (0)
댓글은 익명으로 작성되며, 삭제 비밀번호를 설정하면 본인만 삭제할 수 있습니다. 비밀번호를 설정하지 않은 댓글은 누구나 삭제할 수 있습니다.
HTTP는 평문 전송, 변조 가능, 신원 미확인. RFC 9110/9112 원문 기반으로 무상태 프로토콜의 3가지 보안 한계와 실제 공격 시나리오.
댓글은 익명으로 작성되며, 삭제 비밀번호를 설정하면 본인만 삭제할 수 있습니다. 비밀번호를 설정하지 않은 댓글은 누구나 삭제할 수 있습니다.
1989년 3월, CERN(유럽 입자물리학 연구소)의 팀 버너스리는 연구자들이 문서를 공유할 수 있는 시스템을 제안했다. 1991년 8월, 이 시스템의 통신 규약이 공개됐다. HTTP(Hypertext Transfer Protocol)다.
RFC 9110(HTTP Semantics)은 HTTP를 다음과 같이 정의한다:
"HTTP is defined as a stateless protocol, meaning that each request message's semantics can be understood in isolation." (Section 3.3)
무상태(stateless) 프로토콜 — 서버는 이전 요청을 기억하지 않는다. 모든 요청은 독립적이다. 이 설계가 HTTP의 장점이자 한계의 출발점이다.
http://example.com/index.html을 입력하면 4단계가 순서대로 실행된다.
| 단계 | 동작 | 프로토콜 |
|---|---|---|
| 1. DNS 해석 | example.com → 93.184.216.34 | DNS |
| 2. TCP 연결 | 3-way handshake (SYN → SYN-ACK → ACK) | TCP |
| 3. HTTP 요청 | 클라이언트가 요청 메시지 전송 | HTTP |
| 4. HTTP 응답 | 서버가 응답 메시지 반환 | HTTP |
HTTP/1.0(RFC 1945)에서는 요청마다 새 TCP 연결을 맺었다. 페이지에 이미지가 10개 있으면 TCP 연결도 10번 필요했다.
HTTP/1.1(RFC 9112)에서 이 문제를 해결했다:
"HTTP/1.1 defaults to the use of persistent connections, allowing multiple requests and responses over a single connection." (RFC 9112 Section 9.3)
하나의 TCP 연결을 재사용해 여러 요청을 보낼 수 있게 된 것이다.
RFC 9112 Section 2.1이 정의하는 HTTP 메시지의 구조는 다음과 같다:
HTTP-message = start-line CRLF
*( field-line CRLF )
CRLF
[ message-body ]
실제 요청을 예시로 분해하면:
GET /posts/117 HTTP/1.1 ← 요청 라인 (Request Line)
Host: revelare.kr ← 헤더 필드 시작
User-Agent: Mozilla/5.0
Accept: text/html
Cookie: session_id=abc123
← 빈 줄 (헤더 종료)
← 바디 없음 (GET이므로)
RFC 9112 Section 3의 ABNF 문법:
request-line = method SP request-target SP HTTP-version
method(메서드), 공백(SP), request-target(경로), 공백, HTTP-version이 순서대로 나열된다.
RFC 9110이 정의하는 메서드와 그 속성:
| 메서드 | 속성 (RFC 9110) |
|---|---|
| GET | 안전, 멱등, 캐시 가능 (Section 9.3.1) |
| HEAD | 안전, 멱등, 캐시 가능 (Section 9.3.2) |
| POST | 비안전, 비멱등, 조건부 캐시 (Section 9.3.3) |
| PUT | 비안전, 멱등, 캐시 불가 (Section 9.3.4) |
| DELETE | 비안전, 멱등, 캐시 불가 (Section 9.3.5) |
안전(Safe) 이란 "read-only 의미론" — GET은 서버 상태를 변경하지 않는다는 약속이다(RFC 9110 Section 9.2.1).
멱등(Idempotent) 이란 "동일한 요청을 여러 번 보내도 결과가 같다"는 의미다. PUT으로 같은 데이터를 10번 보내도 서버 상태는 한 번 보낸 것과 동일하다(RFC 9110 Section 9.2.2).
POST는 둘 다 아니다. 같은 POST를 두 번 보내면 리소스가 두 개 생길 수 있다.
RFC 9112 Section 5의 규칙:
field-line = field-name ":" OWS field-value OWS
필드 이름과 콜론 사이에 공백이 있으면 안 된다. 서버는 이를 400 Bad Request로 거부해야 한다(RFC 9112 Section 5.1). 이 규칙을 느슨하게 처리하면 HTTP 요청 스머글링(request smuggling) 공격에 취약해진다.
주요 헤더 예시:
| 헤더 | 역할 | RFC |
|---|---|---|
| Host | 요청 대상 호스트 지정 (가상 호스팅 지원) | 9110 Section 7.2 |
| Content-Type | 본문의 미디어 타입 | 9110 Section 8.3 |
| Authorization | 인증 정보 | 9110 Section 11.6.2 |
| Cookie | 저장된 쿠키 전송 | 6265 Section 4.2 |
Host 헤더는 HTTP/1.0에서는 핵심가 아니었다. HTTP/1.1에서 핵심가 된 이유는 하나의 IP 주소에 여러 도메인을 운영(가상 호스팅)하려면 서버가 어떤 도메인으로 요청이 왔는지 알아야 하기 때문이다.
HTTP/1.1 200 OK ← 상태 라인 (Status Line)
Content-Type: text/html ← 헤더 필드 시작
Content-Length: 1256
Set-Cookie: session_id=xyz789
← 빈 줄
<!DOCTYPE html>... ← 바디
RFC 9112 Section 4:
status-line = HTTP-version SP status-code SP [ reason-phrase ]
| 코드 | 의미 | RFC 9110 |
|---|---|---|
| 200 | 요청 성공 | Section 15.3.1 |
| 201 | 리소스 생성됨 | Section 15.3.2 |
| 301 | 영구 이동 | Section 15.4.2 |
| 302 | 임시 이동 | Section 15.4.3 |
| 400 | 잘못된 요청 | Section 15.5.1 |
| 401 | 인증 필요 | Section 15.5.2 |
| 403 | 접근 거부 | Section 15.5.4 |
| 404 | 리소스 없음 | Section 15.5.5 |
| 500 | 서버 내부 오류 | Section 15.6.1 |
RFC 9110은 404에 대해 이렇게 명시한다:
"A 404 status code does not indicate whether this lack of representation is temporary or permanent." (Section 15.5.5)
또한 403(Forbidden)에 대해서는:
"An origin server that wishes to 'hide' the current existence of a forbidden target resource MAY instead respond with a status code of 404." (Section 15.5.4)
보안 관점에서 중요한 설계다. 리소스의 존재 여부를 숨기고 싶으면 403 대신 404를 반환할 수 있다.
HTTP가 무상태라면, 로그인 상태는 어떻게 유지하는가?
RFC 6265(HTTP State Management Mechanism)가 해결책을 정의한다:
"To store state, the origin server includes a Set-Cookie header in an HTTP response. In subsequent requests, the user agent returns a Cookie request header to the origin server." (Section 3)
동작 흐름:
[클라이언트] [서버]
──── POST /login ────→
(id=user, pw=pass)
←── 200 OK ──────────
Set-Cookie: session_id=abc123
──── GET /dashboard ──→
Cookie: session_id=abc123
←── 200 OK ──────────
(대시보드 HTML)
RFC 6265 Section 4.1.1의 문법:
set-cookie-string = cookie-pair *( ";" SP cookie-av )
| 속성 | 역할 | RFC 6265 |
|---|---|---|
| Domain | 쿠키가 전송되는 도메인 범위 | Section 5.2.3 |
| Path | 쿠키가 전송되는 경로 범위 | Section 5.2.4 |
| Secure | HTTPS에서만 전송 | Section 5.2.5 |
| HttpOnly | JavaScript에서 접근 불가 | Section 5.2.6 |
| Expires / Max-Age | 만료 시점 | Section 5.2.1-2 |
RFC 6265 Section 8.3이 명시하는 핵심 위험:
"Unless sent over a secure channel (such as TLS), the information in the Cookie and Set-Cookie headers is transmitted in the clear."
HTTP에서 쿠키는 평문으로 전송된다. 같은 네트워크의 누구나 세션 ID를 가로챌 수 있다.
HTTP는 데이터를 암호화하지 않는다. 네트워크 경로의 어떤 지점에서든 내용을 읽을 수 있다.
실제 사례: NSA MUSCULAR 프로그램 (2013년 10월 공개)
2013년 에드워드 스노든이 공개한 문서에 따르면, NSA와 영국 GCHQ는 Google과 Yahoo의 데이터센터 간 광케이블을 도청하고 있었다. 데이터센터 내부 통신이 암호화되지 않았기 때문에, 30일간 1억 8,100만 건의 레코드를 수집했다(2012년 12월~2013년 1월). 텍스트, 오디오, 비디오, 메타데이터가 모두 포함됐다.
암호화를 깰 필요가 없었다. 애초에 암호화가 없었다.
HTTP에는 전송된 데이터가 원본과 동일한지 검증하는 메커니즘이 없다. 중간자가 응답을 바꿔도 클라이언트는 알 수 없다.
실제 사례: 중국 Great Cannon의 GitHub DDoS (2015년 3월)
중국 국경 게이트웨이가 Baidu의 JavaScript 파일에 대한 HTTP 응답을 가로채 악성 코드로 교체했다. 교체 비율은 약 2%, 피크 시 17.5%였다. 수백만 사용자의 브라우저가 무기화돼 GitHub에 시간당 26억 건의 요청을 보내는 DDoS 공격이 됐다. 사용자도 Baidu도 응답이 변조됐다는 사실을 감지할 수 없었다.
HTTP는 서버의 신원을 확인하는 방법을 제공하지 않는다. DNS 응답을 조작하거나 네트워크 경로를 통제하면 가짜 서버로 유도할 수 있다.
RFC 9110 Section 17.1이 이 문제를 인정한다:
"Any attack on a user's network host table, cached names, or name resolution libraries becomes an avenue for attack on establishing authority for 'http' URIs."
실제 사례: 튀니지 정부의 국가 단위 자격증명 탈취 (2011년 1월)
튀니지 혁명 당시, 정부는 ISP를 통해 Facebook, Gmail, Yahoo의 HTTP 로그인 페이지에 JavaScript를 주입했다. 주입된 코드는 사용자 이름과 비밀번호를 수집해 정부 서버로 전송했다. 수집된 자격증명으로 시민들의 계정에 접속해 메시지를 내려받고, 계정 자체를 삭제했다. 이 공격은 전체 튀니지 인터넷 사용자에게 영향을 미쳤다.
| 버전 | 연도 | 주요 변화 |
|---|---|---|
| HTTP/0.9 | 1991 | GET만 지원, 헤더 없음, 상태 코드 없음 |
| HTTP/1.0 | 1996 (RFC 1945) | 헤더, POST/HEAD 메서드, Content-Type 추가 |
| HTTP/1.1 | 1997 (RFC 2068) | 지속 연결, Host 핵심, 청크 전송, 파이프라이닝 |
| HTTP/2 | 2015 (RFC 7540) | 멀티플렉싱, 헤더 압축, 서버 푸시, 바이너리 프로토콜 |
| HTTP/3 | 2022 (RFC 9114) | TCP를 QUIC(UDP)로 대체, TLS 1.3 내장 |
HTTP/1.0에서 HTTP/1.1로의 핵심 변화:
| HTTP/1.0 | HTTP/1.1 |
|---|---|
| 요청마다 새 TCP 연결 | 지속 연결 기본값 |
| Host 헤더 선택 사항 | Host 헤더 핵심 |
| 청크 전송 미지원 | 청크 전송 지원 |
| 콘텐츠 협상 부록 | 콘텐츠 협상 핵심 기능 |
2010년 10월, 시애틀의 프로그래머 에릭 버틀러가 Firesheep이라는 Firefox 확장을 공개했다. 공개 와이파이에서 HTTP 세션 쿠키를 자동으로 가로채 다른 사람의 Facebook, Twitter 계정에 로그인하는 도구였다. 기술 지식이 없는 사람도 확장을 설치하고 카페에 앉으면 주변 사람의 세션을 탈취할 수 있었다.
Firesheep은 HTTP의 3개 한계를 한 번에 증명했다. 평문 전송(쿠키 가로채기), 무결성 부재(조작 가능), 신원 미확인(가짜 응답 가능).
이후 Facebook과 Twitter는 수개월 내에 전 사이트 HTTPS를 도입했다. 2014년 Google은 HTTPS를 검색 순위 신호로 도입했고, 2015년 Let's Encrypt가 무료 인증서를 제공하면서 HTTPS 도입 비용 장벽이 사라졌다. 2018년 Chrome 68은 모든 HTTP 사이트에 "주의 요함" 경고를 표시하기 시작했다.
2026년 현재, 전체 웹사이트의 88.6% 가 HTTPS를 기본으로 사용한다(W3Techs, 2026년 2월). Chrome 브라우저의 99% 이상의 탐색 시간이 HTTPS 페이지에서 발생한다(Google Transparency Report, 2025).
HTTP의 한계를 해결한 것이 HTTPS다. 다음 글에서는 이 구조를 악용하는 프로토콜 수준의 공격 기법을, 이어서 HTTPS가 HTTP의 3개 한계를 어떻게 해결하는지 다룬다.
HTTP/1.0에서는 요청마다 TCP 연결을 새로 맺었다. 3-way handshake 비용이 매 요청에 반복되는 비효율을 해결하기 위해 HTTP/1.1은 Keep-Alive를 기본 활성화했다. 하나의 TCP 연결로 여러 요청을 순차 처리한다.
하지만 파이프라이닝(Pipelining)은 실패한 표준이 됐다. 여러 요청을 응답을 기다리지 않고 연속 전송하는 방식인데, Head-of-Line(HOL) Blocking 문제를 해결하지 못했다. 첫 번째 응답이 지연되면 뒤따르는 모든 응답이 대기해야 한다. 대부분의 브라우저는 파이프라이닝을 비활성화했다.
HTTP/2는 하나의 TCP 연결에서 여러 스트림을 동시에 처리한다. 각 스트림은 독립적인 요청-응답 쌍이며, 프레임 단위로 인터리빙된다. 100개의 리소스를 요청해도 단일 연결로 병렬 전송이 가능하다.
HPACK 헤더 압축도 핵심 개선이다. HTTP/1.1에서 매 요청마다 반복되는 Cookie, User-Agent 같은 헤더를 정적/동적 테이블로 압축한다. 구글 측정에 따르면 평균 헤더 크기가 500바이트에서 30바이트로 줄었다.
HTTP/2의 근본적 문제는 TCP 위에서 동작한다는 점이다. TCP 계층의 패킷 손실이 모든 스트림에 영향을 미치는 TCP HOL Blocking이 발생한다. HTTP/3는 UDP 기반의 QUIC 프로토콜 위에서 동작하여 이 문제를 해결했다.
QUIC은 연결 설정과 TLS 핸드셰이크를 하나의 라운드트립으로 통합한다. Wi-Fi에서 셀룰러로 전환해도 Connection ID 기반으로 연결이 유지된다. 2024년 기준 전 세계 웹 트래픽의 약 30%가 HTTP/3를 사용한다(W3Techs).
쿠키 기반 세션의 보안 취약점은 잘 알려져 있다. Session Fixation, Session Hijacking, CSRF 공격이 대표적이다. HttpOnly, Secure, SameSite=Strict 속성은 핵심 방어 수단이지만, 이 세 가지를 모두 적용한 사이트는 2024년 기준 전체의 42%에 불과하다(HTTP Archive).
JWT(JSON Web Token)가 쿠키 기반 세션의 대안으로 부상했지만, 새로운 공격 표면을 만들었다. 토큰 크기 증가, 토큰 무효화의 어려움, 알고리즘 혼동 공격 등이 대표적 이슈다. 결국 HTTP의 무상태 설계를 보완하는 모든 방법은 보안과 편의성 사이의 균형이다.
HTTP/1.1의 가장 큰 병목은 Head-of-Line Blocking이다. 하나의 TCP 연결에서 요청은 순서대로 처리되어야 한다. 첫 번째 요청의 응답이 느리면 뒤따르는 모든 요청이 대기한다. 브라우저는 이를 우회하기 위해 도메인당 6개의 TCP 연결을 동시에 열지만, 이는 서버 자원 낭비와 TCP 혼잡 제어 경쟁을 유발한다.
HTTP/2는 멀티플렉싱으로 이 문제를 해결했다. 하나의 TCP 연결 위에 여러 스트림을 동시에 운영하여, 각 스트림이 독립적으로 요청과 응답을 주고받는다. 헤더 압축(HPACK)도 도입됐다. HTTP/1.1에서는 매 요청마다 수백 바이트의 헤더가 반복 전송됐지만, HPACK은 정적 테이블과 동적 테이블을 사용해 중복 헤더를 인덱스 번호 하나로 대체한다.
서버 푸시(Server Push)는 클라이언트가 요청하기 전에 리소스를 전송하는 기능이다. HTML 파일을 요청받은 서버가 CSS와 JavaScript 파일도 함께 푸시하면 추가 왕복 시간(RTT)을 절약할 수 있다. 그러나 실제로는 브라우저 캐시와의 충돌, 불필요한 데이터 전송 문제로 Chrome은 2022년에 서버 푸시 지원을 제거했다.
HTTP/2가 멀티플렉싱을 도입했지만, TCP 계층의 Head-of-Line Blocking은 여전히 존재한다. TCP 패킷 하나가 손실되면 이 연결의 모든 스트림이 재전송을 기다려야 한다. 패킷 손실률이 2%만 되어도 HTTP/2의 성능은 HTTP/1.1보다 떨어질 수 있다.
HTTP/3는 TCP 대신 UDP 기반의 QUIC 프로토콜을 사용한다. QUIC은 각 스트림을 독립적으로 관리하여, 한 스트림의 패킷 손실이 다른 스트림에 영향을 주지 않는다. 연결 설정도 빠르다. TCP+TLS는 2-3 RTT가 필요하지만, QUIC은 TLS 1.3을 내장하여 1 RTT로 연결과 암호화를 동시에 완료한다.
Connection Migration도 QUIC의 핵심 기능이다. TCP 연결은 (소스IP, 소스포트, 대상IP, 대상포트)의 4-tuple로 식별된다. WiFi에서 LTE로 전환하면 소스IP가 바뀌어 연결이 끊긴다. QUIC은 Connection ID로 연결을 식별하므로 네트워크 전환 시에도 연결이 유지된다.
HTTP의 무상태 특성을 보완하기 위해 쿠키가 도입됐지만, 쿠키 자체가 새로운 공격 표면이 됐다. CSRF(Cross-Site Request Forgery)는 브라우저가 쿠키를 자동 첨부하는 특성을 악용한다. 공격자의 웹사이트에서 은행 API를 호출하면, 브라우저는 은행 도메인의 세션 쿠키를 자동으로 포함시킨다.
SameSite 쿠키 속성은 이 문제를 완화한다. SameSite=Strict은 다른 사이트에서의 모든 요청에 쿠키를 제외하고, SameSite=Lax는 GET 요청에만 허용한다. Chrome은 2020년부터 명시적 SameSite 설정이 없는 쿠키를 Lax로 기본 처리한다.
세션 고정(Session Fixation) 공격도 주의가 필요하다. 공격자가 미리 생성한 세션 ID를 피해자에게 사용하게 만들면, 피해자가 로그인한 후 같은 세션 ID로 접근할 수 있다. 로그인 성공 시 세션 ID를 재발급하는 것이 표준 대응 방안이다.
HTTP의 무상태 한계를 근본적으로 넘어서는 프로토콜이 WebSocket이다. HTTP 핸드셰이크로 연결을 시작한 후 TCP 위에서 양방향 실시간 통신 채널을 유지한다. 채팅, 실시간 알림, 주식 시세, 온라인 게임 등 서버가 클라이언트에게 능동적으로 데이터를 전송해야 하는 모든 경우에 사용된다.
WebSocket 연결은 HTTP Upgrade 메커니즘으로 설정된다. 클라이언트가 Upgrade: websocket 헤더를 포함한 HTTP 요청을 보내면, 서버가 101 Switching Protocols로 응답하며 프로토콜이 전환된다. 이후 통신은 HTTP가 아닌 WebSocket 프레임으로 이루어진다.
Server-Sent Events(SSE)는 서버에서 클라이언트로의 단방향 스트리밍만 필요할 때 더 적합한 대안이다. HTTP 연결 위에서 동작하므로 프록시와 방화벽 호환성이 좋고, 자동 재연결 기능이 내장되어 있다. ChatGPT의 스트리밍 응답이 SSE로 구현된 대표적 사례다.
HTTP의 진화는 웹의 요구사항과 함께 계속되고 있다. 무상태 프로토콜로 시작한 HTTP는 쿠키, 세션, WebSocket, SSE 등의 확장을 통해 상태 유지 통신을 지원하게 됐다. 그러나 이런 확장은 모두 원래 설계의 한계를 우회하는 것이며, 각각 새로운 보안 고려사항을 수반한다.
AI 활용 안내 이 글은 AI(Claude)의 도움을 받아 작성되었습니다. 인용된 통계와 사례는 참고 자료에 명시된 출처에 근거하며, 설명을 위한 일부 표현은 각색되었습니다.
면책 조항 본 글은 보안 인식 제고를 위한 교육 목적으로 작성되었습니다. 언급된 공격 기법을 실제로 시도하는 행위는 「정보통신망법」, 「형법」 등에 따라 처벌받을 수 있으며, 본 블로그는 이에 대한 법적 책임을 지지 않습니다.