사전 RFC 9112
표준

RFC 9112

gabury1고친 사람 github-actions[bot]

RFC 9112 는 웹 서버와 브라우저가 같은 바이트를 같은 메시지로 끊어 읽게 해 주는 표준 문서입니다. 요청과 응답을 어떤 글자 모양으로 적는지, 본문이 어디서 끝나는지를 이 문서가 정합니다. 한 번 맺은 연결을 언제까지 쓰고 언제 닫는지도 함께 정합니다.

쉽고 빠른 이해

RFC 9112 는 웹 요청과 응답을 바이트로 적는 법을 정한 문서입니다. 브라우저가 서버에 보내는 GET /where?q=now HTTP/1.1 한 줄과 그 아래 줄들의 생김새가 이 규칙을 따릅니다.

연결 위로는 바이트가 끊김 없이 흘러옵니다. 규칙이 없으면 장비마다 메시지의 끝을 다르게 잡습니다. 그 틈으로 숨겨 둔 요청이 끼어들 수 있습니다.

돌아가는 방식은 이렇습니다.

  1. 첫 줄에 요청 내용이나 결과를 적습니다
  2. 이름과 값을 짝지은 줄을 늘어놓습니다. 빈 줄 하나가 그 끝입니다
  3. 본문 길이는 그 줄들에 적힌 바이트 수로 정합니다. 길이를 미리 모르면 본문을 조각으로 나눠 보냅니다. 크기 0 인 조각이 끝 표시입니다

웹 요청을 1.1 판 방식으로 주고받는 서버·브라우저·프록시가 이 규칙을 따릅니다. 뒤에 나온 2 판과 3 판은 같은 요청과 응답을 다른 방식으로 끊어 나릅니다. 그 판들에는 이 끊어 읽기 규칙이 안 걸립니다.

대가도 있습니다. 응답에는 어느 요청의 답인지 적는 칸이 없습니다. 그래서 한 연결에서는 응답이 요청을 보낸 순서대로만 돌아옵니다.

상세

RFC(Request for Comments, 의견 요청)는 IETF(Internet Engineering Task Force, 인터넷 기술 표준을 만드는 단체)가 번호를 붙여 내는 문서입니다. RFC 9112 는 웹이 쓰는 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜)의 1.1 판을 다룹니다. 문서 제목도 「HTTP/1.1」입니다.

메시지는 요청 하나 또는 응답 하나를 이루는 바이트 묶음입니다. 이 문서가 정하는 것은 넷입니다. 메시지를 적는 문법, 받은 바이트를 메시지로 끊어 읽는 법, 연결 관리, 그리고 이 셋에 걸린 보안 문제입니다.

목표는 두 쪽이 지킬 요구를 빠짐없이 적는 것입니다. 하나는 메시지를 끊어 읽는 프로그램입니다. 다른 하나는 메시지를 받아 다음 장비로 넘기는 중계 장비입니다. 중계 장비의 대표는 프록시입니다. 프록시는 클라이언트의 요청을 대신 받아 다른 서버로 넘기는 장비입니다.

연결 위로는 바이트가 끊김 없이 흘러옵니다. 받는 쪽은 그 흐름에서 메시지 하나의 끝을 스스로 찾아야 합니다. 요청이 여러 장비를 거칠 때 장비마다 끝을 다르게 잡으면 문제가 생깁니다. 멀쩡해 보이는 요청 안에 다른 요청이 숨을 수 있습니다.

이렇게 요청을 숨기는 공격의 이름이 요청 스머글링입니다. 이 문서는 그 틈을 줄이려고 끊어 읽는 규칙을 촘촘하게 적습니다.

HTTP/1.1 은 이 문서 하나로 정해지지 않습니다. 세 문서가 나눠 정합니다.

문서 제목 맡는 것
RFC 9110 HTTP Semantics 요청과 응답이 무슨 뜻인지
RFC 9111 HTTP Caching 캐시
RFC 9112 HTTP/1.1 그 뜻을 1.1 판의 문법과 연결 위에 싣는 법

이 문서는 요구마다 강도를 대문자 영어 낱말로 붙입니다. 이 편은 그 낱말을 문장 끝 괄호에 옮겨 둡니다. 낱말은 셋입니다. NOT 이 붙으면 하지 말라는 쪽이 됩니다.

낱말 뜻
MUST · MUST NOT 반드시 지킵니다. 어기면 표준을 안 지킨 것입니다
SHOULD · SHOULD NOT 권고입니다. 이유가 있으면 어겨도 됩니다
MAY 해도 되고 안 해도 됩니다

메시지의 모양

HTTP/1.1 메시지는 네 부분이 위에서 아래로 놓입니다. 첫 줄인 시작줄, 이름과 값을 짝지은 필드 줄 여럿, 빈 줄 하나, 그리고 있을 때만 붙는 본문입니다. 필드 줄 묶음을 헤더라고 부릅니다.

flowchart TD
    M["메시지"] --> S["시작줄"]
    M --> F["필드 줄 · 0개 이상 · 묶어서 헤더"]
    M --> E["빈 줄"]
    M --> B["본문 · 있을 때만"]
    S --> R["요청줄 · 요청일 때"]
    S --> T["상태줄 · 응답일 때"]

시작줄과 필드 줄은 CRLF(Carriage Return Line Feed, 줄 바꿈을 나타내는 두 바이트)로 끝납니다. 빈 줄은 CRLF 하나만 있는 줄입니다. 받는 쪽은 이 빈 줄을 보고 헤더가 끝났다고 압니다.

요청과 응답이 갈리는 곳은 둘뿐입니다. 시작줄, 그리고 본문 길이를 정하는 방법입니다.

이론으로는 시작줄 모양만 보고 받은 것이 요청인지 응답인지 가를 수 있습니다. 구현은 그렇게 가르지 않습니다. 서버 구현은 요청만 온다고 가정합니다. 응답이 오면 알 수 없는 메서드를 쓴 잘못된 요청으로 읽습니다. 클라이언트 구현은 응답만 온다고 가정합니다.

받는 쪽은 메시지를 문자열이 아니라 바이트열로 읽어야 합니다(MUST). 그 바이트는 ASCII(American Standard Code for Information Interchange, 미국 정보 교환 표준 부호)를 포함하는 인코딩으로 봅니다. 유니코드 문자열로 읽으면 깨진 멀티바이트 문자를 라이브러리마다 다르게 처리합니다. 그 안에 줄 바꿈 바이트가 들어 있으면 보안 취약점이 생깁니다.

줄 끝에는 약간의 너그러움이 허용됩니다. 받는 쪽은 앞에 CR(Carriage Return) 없이 LF(Line Feed) 하나만 와도 줄 끝으로 받아 줄 수 있습니다(MAY). 반대로 LF 가 뒤따르지 않는 CR 하나는 본문 밖에서 만들면 안 됩니다(MUST NOT).

요청줄과 상태줄

시작줄은 빈칸 하나로 나뉜 세 토막입니다. 요청이면 요청줄, 응답이면 상태줄이라고 부릅니다.

첫 토막 둘째 토막 셋째 토막
요청줄 메서드 GET 요청 대상 /where?q=now 프로토콜 판 HTTP/1.1
상태줄 프로토콜 판 HTTP/1.1 상태 코드 200 사유 문구 OK

메서드는 요청이 원하는 동작의 이름입니다. 상태 코드는 요청 결과를 알리는 세 자리 숫자입니다.

요청 대상은 요청이 가리키는 자원입니다. 적는 꼴은 넷입니다. 어느 꼴을 쓸지는 메서드와 받는 쪽에 따라 갈립니다.

자원을 가진 서버를 원 서버라고 합니다. 원 서버에 바로 보내면 주소의 경로와 질의만 적습니다. 프록시에 보내면 주소 전체를 적습니다.

나머지 두 꼴은 메서드 둘만 씁니다. CONNECT 는 프록시를 거쳐 터널을 여는 메서드입니다. 터널은 프록시가 바이트를 해석하지 않고 양쪽으로 이어 주는 통로입니다. OPTIONS 를 서버 전체에 보낼 때도 따로 꼴이 있습니다. 네 꼴의 실물은 「예시」 절에 있습니다.

사유 문구는 상태 코드를 사람이 읽게 풀어 쓴 글입니다. 클라이언트에게는 이 글을 무시하기를 권합니다 (SHOULD). 이 글은 믿을 만한 정보 통로가 아닙니다. 지역 말로 번역되거나, 중계 장비가 덮어쓰거나, 다른 판의 HTTP 로 넘어갈 때 버려질 수 있습니다. 사유 문구가 비어도 서버는 상태 코드 뒤의 빈칸을 보내야 합니다(MUST). 그래서 상태줄이 빈칸으로 끝나기도 합니다.

필드 줄

필드 줄은 이름, 콜론, 값 순서로 적습니다. 이름은 대소문자를 가리지 않습니다. 값 앞뒤의 빈칸은 값에 들지 않습니다.

필드 줄은 이름과 상관없이 한 가지 방법으로 끊어 읽습니다. 값 안을 따지는 일은 대개 헤더를 다 읽은 뒤로 미룹니다.

이름과 콜론 사이에는 빈칸을 넣을 수 없습니다. 이 빈칸을 장비마다 다르게 다룬 탓에 요청을 어디로 보낼지 정하는 과정과 응답 처리에서 보안 취약점이 난 적이 있습니다. 그런 요청을 받은 서버가 할 일은 「실패」 절에 있습니다.

요청에는 Host 필드를 반드시 넣습니다(MUST). Host 는 요청이 찾아간 호스트 이름과 포트를 담습니다. 서버 한 대가 여러 호스트 이름의 요청을 받을 때 이 값으로 자원을 가립니다.

본문의 끝을 정하는 방법

이 소절은 받는 쪽이 헤더를 다 읽은 뒤 본문을 몇 바이트 읽을지 정하는 순서를 다룹니다. 재료는 필드 둘과 시작줄입니다.

Content-Length 는 본문의 바이트 수를 십진수로 적는 필드입니다.

전송 인코딩은 나르는 동안에만 본문에 입히는 변환입니다. 대표가 청크 전송 인코딩(chunked)입니다. 본문을 조각으로 나눠 보내는 방식입니다.

Transfer-Encoding 은 본문에 입힌 전송 인코딩의 이름을 입힌 순서대로 적는 필드입니다.

필드를 보기 전에 시작줄만으로 정해지는 응답이 있습니다. HEAD 요청에 대한 응답, 그리고 1xx(Informational)·204(No Content)·304(Not Modified) 응답은 헤더 뒤 첫 빈 줄에서 끝납니다. 어떤 필드가 붙어 있어도 본문이 없습니다. CONNECT 요청에 대한 2xx 응답은 빈 줄 바로 뒤부터 연결이 터널이 됩니다.

그 밖의 메시지는 아래 순서로 따집니다.

flowchart TD
    B{"Transfer-Encoding 이 있나"}
    B -->|있다| C{"청크 전송 인코딩이 맨 뒤에 있나"}
    C -->|있다| D["길이 0 인 청크가 나올 때까지 읽는다"]
    C -->|없다| E["응답은 연결이 닫힐 때까지 읽는다 · 요청은 400 으로 거절한다"]
    B -->|없다| F{"Content-Length 가 있나"}
    F -->|있다| G["적힌 바이트 수만큼 읽는다"]
    F -->|없다| H["요청은 본문이 없다 · 응답은 연결이 닫힐 때까지 읽는다"]

두 필드가 함께 오면 Transfer-Encoding 이 이깁니다. 이런 메시지는 요청을 숨기거나 응답을 둘로 쪼개려는 공격의 시도일 수 있습니다. 두 공격의 꼴은 「실패」 절에 있습니다.

이런 메시지는 오류로 다루는 것이 마땅합니다. 다만 이 요구에는 MUST 같은 대문자 낱말이 없습니다. 강도가 걸린 요구는 아닙니다. 이런 메시지를 다음 장비로 넘기는 중계 장비는 먼저 Content-Length 를 지우고 Transfer-Encoding 대로 처리해야 합니다(MUST).

요청은 연결을 닫는 것으로 끝을 알리지 않습니다. 두 필드가 다 없으면 요청은 헤더 바로 뒤에서 끝납니다. 응답은 연결이 닫히는 것으로 끝날 수 있습니다. 다만 다 받은 응답과 네트워크가 끊겨 덜 받은 응답을 구별할 방법이 없습니다. 그래서 서버에게는 되도록 길이나 청크로 끝을 적어 보내기를 권합니다(SHOULD). 연결을 닫아 끝을 알리는 방식은 주로 HTTP/1.0 과 맞추려고 남아 있습니다.

청크 전송 인코딩

만들면서 내보내는 본문은 다 만들기 전에는 길이를 모릅니다. 청크 전송 인코딩은 그런 본문을 조각 여럿으로 나눠 보냅니다. 조각 하나가 청크입니다. 청크마다 앞에 자기 크기를 적습니다. 보내는 쪽은 연결을 닫지 않고도 끝을 알릴 수 있습니다. 받는 쪽은 본문을 다 받았는지 압니다.

아래 그림은 청크로 나눈 본문 안에서 무엇이 어떤 순서로 놓이는지 보입니다.

block-beta
columns 2
  a["청크 크기 · 십육진수"]:1 b["청크 데이터"]:1
  c["청크 크기 · 십육진수"]:1 d["청크 데이터"]:1
  e["크기 0 인 마지막 청크"]:2
  f["트레일러 필드 줄 · 있을 때만"]:2
  g["빈 줄"]:2

크기는 데이터가 몇 바이트인지를 십육진수로 적습니다. 크기 0 인 청크가 오면 본문이 끝납니다. 그 뒤에 트레일러가 붙을 수 있습니다. 마지막은 빈 줄 하나입니다.

트레일러는 본문을 보내는 동안 만들어지는 정보를 맨 끝에 붙이는 필드 줄입니다. 무결성 검사값, 전자 서명, 후처리 결과를 여기에 싣습니다.

받는 쪽은 청크 전송 인코딩을 반드시 해독할 수 있어야 합니다(MUST). 보내는 쪽은 한 본문에 청크를 두 번 입히면 안 됩니다(MUST NOT). 요청 본문에 다른 전송 인코딩을 입혔다면 청크를 맨 마지막에 입혀야 합니다(MUST). 응답이라면 청크를 맨 마지막에 입히거나 연결을 닫아 끝을 알려야 합니다(MUST).

한 연결을 나눠 쓰는 방법

HTTP/1.1 은 지속 연결이 기본입니다. 지속 연결은 요청과 응답 여럿을 한 연결에 실어 나르는 방식입니다. 구현에는 지속 연결을 지원하기를 권합니다(SHOULD).

Connection 필드는 지금 연결에 거는 제어 옵션을 적는 필드입니다. keep-alive 는 HTTP/1.0 메시지가 연결을 이어 쓰자고 할 때 이 필드에 적는 옵션입니다.

받는 쪽은 가장 최근에 받은 메시지의 프로토콜 판과 Connection 필드를 보고 연결을 이어 쓸지 정합니다. 위에서부터 따져 처음 맞는 줄을 따릅니다.

가장 최근 메시지 지금 응답 뒤 연결
Connection 필드에 close 옵션이 있다 닫는다
프로토콜 판이 HTTP/1.1 이상이다 이어 쓴다
HTTP/1.0 메시지의 Connection 필드에 keep-alive 가 있다. 받는 쪽이 프록시가 아니거나 메시지가 응답이다. 받는 쪽이 keep-alive 를 따르기로 한다 이어 쓴다
그 밖 닫는다

프록시가 받은 HTTP/1.0 요청은 keep-alive 가 있어도 셋째 줄에 맞지 않습니다. 그 연결은 넷째 줄대로 응답 뒤에 닫힙니다.

연결을 이어 쓰려면 모든 메시지가 연결을 닫지 않고도 끝을 알 수 있어야 합니다. 서버는 요청 본문을 끝까지 읽거나 응답 뒤 연결을 닫아야 합니다(MUST). 덜 읽은 바이트가 다음 요청으로 잘못 읽히기 때문입니다.

지속 연결을 쓰는 클라이언트는 응답을 기다리지 않고 요청 여럿을 이어 보낼 수 있습니다(MAY). 이렇게 보내는 방식이 파이프라이닝입니다.

HTTP/1.1 메시지에는 요청 식별자가 없습니다. 응답은 같은 연결에서 요청을 보낸 순서대로 짝지어집니다. 서버는 안전한 메서드로 된 요청들이면 동시에 처리해도 됩니다(MAY). 안전한 메서드는 서버의 상태를 바꾸지 않는 읽기용 메서드입니다. 처리는 동시에 해도 응답은 요청을 받은 순서대로 보내야 합니다(MUST).

sequenceDiagram
    participant 클라이언트
    participant 서버
    클라이언트->>서버: 요청 1
    클라이언트->>서버: 요청 2
    Note over 서버: 응답 2 를 먼저 만들었어도 응답 1 보다 먼저 보내지 못한다
    서버-->>클라이언트: 응답 1
    서버-->>클라이언트: 응답 2

파이프라이닝한 클라이언트가 응답을 다 받기 전에 연결이 닫힐 수 있습니다. 그러면 그 클라이언트에게는 답을 못 받은 요청을 다시 보내기를 권합니다(SHOULD).

서버가 마지막 응답에서 close 로 알리지 않은 채 끊긴 연결도 있습니다. 이런 연결의 요청을 다시 보낼 때는 새 연결을 맺자마자 파이프라이닝하면 안 됩니다(MUST NOT). 앞 연결에 남은 요청 가운데 첫 요청이 오류 응답을 불렀을 수 있기 때문입니다. 클라이언트는 그 오류 응답을 앞 연결과 함께 이미 한 번 잃었습니다. 새 연결에 요청을 여럿 몰아 보냈다가 그 연결도 일찍 닫히면 같은 오류 응답을 또 잃을 수 있습니다.

멱등 메서드는 여러 번 보내도 한 번 보낸 것과 결과가 같은 메서드입니다. 그래서 연결이 끊긴 뒤 자동으로 다시 보낼 수 있습니다. 멱등이 아닌 메서드를 보냈다면 그 최종 응답을 받기 전까지 뒤 요청을 파이프라이닝하지 않기를 권합니다(SHOULD NOT). 일부만 실패한 상황을 알아채고 되돌릴 수단이 있으면 예외입니다.

연결을 그만 쓰려는 쪽에는 Connection 필드에 close 를 적어 알리기를 권합니다(SHOULD). 요청에 적으면 그 연결의 마지막 요청이라는 뜻입니다. 응답에 적으면 그 응답 뒤 서버가 연결을 닫는다는 뜻입니다.

출처 문서

이 절은 RFC 9112 의 번호·판·상태와 폐기 관계를 적습니다. 본문 규칙이 원문 몇 절에서 왔는지도 함께 적습니다. 정본은 rfc-editor.org 가 내는 RFC 9112 원문입니다. 문서 머리는 이렇습니다.

Internet Engineering Task Force (IETF)                  R. Fielding, Ed.
Request for Comments: 9112                                         Adobe
STD: 99                                               M. Nottingham, Ed.
Obsoletes: 7230                                                   Fastly
Category: Standards Track                                J. Reschke, Ed.
ISSN: 2070-1721                                               greenbytes
                                                               June 2022

                                HTTP/1.1

인터넷 표준 단계에 오른 RFC 는 STD(Standard, 표준) 번호를 하나 더 받습니다.

항목 값
번호 RFC 9112 · STD 99
제목 HTTP/1.1
분류 Standards Track
상태 Internet Standard (rfc-editor.org 상태 페이지, 2026-10-02 확인)
발행 2022년 6월
편집자 R. Fielding (Adobe) · M. Nottingham (Fastly) · J. Reschke (greenbytes)
폐기한 문서 RFC 7230 의 일부
이 문서를 갱신한 문서 RFC 9931 (2026년 3월)
이 문서를 폐기한 문서 없음 (상태 페이지 기준)

폐기와 갱신

RFC 7230 은 두 문서가 나눠서 폐기했습니다. HTTP/1.1 메시지와 연결 관리에 관한 부분은 RFC 9112 가 폐기했습니다. 나머지 부분은 RFC 9110 이 폐기했습니다.

RFC 9112 의 문서 머리에는 「Obsoletes: 7230」이 적혀 있습니다. 초록은 더 좁게 「RFC 7230 의 일부를 폐기한다」고 적습니다. RFC 9112 혼자 RFC 7230 전부를 갈음한 것은 아닙니다.

RFC 9931 은 2026년 3월에 나왔습니다. 클라이언트가 연결을 다른 프로토콜로 바꾸자고 요청한 뒤, 그 요청이 확정되기 전에 보낸 데이터의 보안 문제를 다룹니다. 그 문서의 8절은 CONNECT 에 대한 요구를 RFC 9112 에 더합니다. 이 요구는 HTTP/1.1 에만 걸립니다.

flowchart TD
    R9112["RFC 9112 · 2022년 6월"] -->|메시지와 연결 관리 부분을 폐기| R7230["RFC 7230"]
    R9110["RFC 9110 · 2022년 6월"] -->|나머지 부분을 폐기| R7230
    R9931["RFC 9931 · 2026년 3월"] -->|요구를 더해 갱신| R9112

RFC 7230 에서 바뀐 것 가운데 굵직한 것은 이렇습니다(부록 C.3).

바뀐 것 원문 절
설계 목표·역사·구조·적합성·버전·주소·라우팅·헤더 필드 일반 설명 대부분을 RFC 9110 으로 옮겼다. 이 문서에는 HTTP/1.1 고유의 메시지 문법과 연결 관리 요구만 남았다 부록 C.3
본문 밖에서 LF 없이 홀로 선 CR 을 금지했다 §2.2
CONNECT 의 요청 대상을 포트를 생략할 수 있던 꼴에서 호스트:포트 꼴로 좁혔다 §3.2.3
메시지 경계가 모호할 때 받는 쪽이 요청 스머글링·응답 스플리팅 공격(「실패」 절)을 피하도록 요구했다 §6.1

요구 강도

MUST · SHOULD · MAY 같은 낱말은 BCP(Best Current Practice, 현행 최선 관행) 14, 곧 RFC 2119 와 RFC 8174 가 정한 뜻으로 읽습니다. 전부 대문자로 적혔을 때만 그렇게 읽습니다. 적합성 기준과 오류 처리에 대한 고려는 RFC 9110 의 2절이 정합니다.

낱말마다의 뜻은 「상세」 첫머리의 표에 있습니다. 본문에 나온 규칙과 원문 절은 이렇게 맞물립니다.

규칙 원문 절 강도
메시지를 바이트열로 읽는다 §2.2 MUST
LF 하나만 와도 줄 끝으로 받아 준다 §2.2 MAY
본문 밖에서 홀로 선 CR 을 만들지 않는다 §2.2 MUST NOT
요청에 Host 필드를 넣는다 §3.2 MUST
사유 문구를 무시한다 §4 SHOULD
사유 문구가 비어도 그 앞 빈칸을 보낸다 §4 MUST
두 필드가 함께 온 메시지를 넘기기 전에 Content-Length 를 지운다 §6.3 MUST
길이나 청크로 끝을 적어 보낸다 §6.3 SHOULD
청크 전송 인코딩을 해독한다 §6.1 · §7.1 MUST
지속 연결을 지원한다 §9.3 SHOULD
파이프라이닝된 요청의 응답을 받은 순서대로 보낸다 §9.3.2 MUST
끊긴 연결 뒤 새 연결에서 곧바로 파이프라이닝하지 않는다 §9.3.2 MUST NOT
멱등이 아닌 메서드 뒤에 파이프라이닝하지 않는다 §9.3.2 SHOULD NOT
연결을 닫으려면 close 를 적어 알린다 §9.6 SHOULD

거절과 연결 닫기에 걸린 강도는 「실패」 절의 표가 갖습니다. 그 표의 조건은 §2.2 · §3 · §3.2 · §5.1 · §5.2 · §6.1 · §6.3 · §8 · §9.2 에서 왔습니다. 응답 스플리팅은 §11.1, 요청 스머글링은 §11.2 입니다.

예시

이 절에는 원문에 실린 메시지 값이 셋 있습니다. 요청 대상의 네 꼴, 요청과 응답 한 벌, 전송 인코딩 값 한 줄입니다.

요청 대상의 네 꼴

꼴 언제 쓰나 요청줄
origin-form 원 서버에 바로 보낼 때 GET /where?q=now HTTP/1.1
absolute-form 프록시에 보낼 때 GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1
authority-form CONNECT 로 터널을 열 때 CONNECT www.example.com:80 HTTP/1.1
asterisk-form 서버 전체에 OPTIONS 를 물을 때 OPTIONS * HTTP/1.1

http://www.example.org/where?q=now 를 원 서버에서 바로 받으려는 클라이언트는 호스트 www.example.org 의 80번 포트로 TCP(Transmission Control Protocol, 전송 제어 프로토콜) 연결을 열거나 이미 열린 연결을 다시 씁니다. 그리고 이 두 줄을 보냅니다.

GET /where?q=now HTTP/1.1
Host: www.example.org

주소의 경로와 질의만 요청 대상에 적습니다(MUST). 호스트 이름은 Host 필드로 갑니다.

프록시에 보내는 absolute-form 에도 클라이언트는 Host 를 넣어야 합니다(MUST). Host 를 모르는 옛 HTTP/1.0 프록시를 거쳐도 호스트 정보가 넘어가게 하려는 것입니다. absolute-form 을 받은 프록시는 받은 Host 를 버리고, 요청 대상에 적힌 호스트로 Host 를 새로 만들어야 합니다(MUST).

asterisk-form 은 프록시를 거치며 생기기도 합니다. 클라이언트가 프록시에 이렇게 보냈다고 합시다.

OPTIONS http://www.example.org:8001 HTTP/1.1

주소에 경로도 질의도 없습니다. 마지막 프록시는 www.example.org 의 8001번 포트에 연결한 뒤 요청 대상을 * 로 바꿔 넘겨야 합니다(MUST).

OPTIONS * HTTP/1.1
Host: www.example.org:8001

요청과 응답 한 벌

http://www.example.com/hello.txt 에 대한 GET 교환입니다. RFC 9110 이 HTTP/1.1 의 전형적인 교환으로 싣는 값입니다. 먼저 요청입니다.

GET /hello.txt HTTP/1.1
User-Agent: curl/7.64.1
Host: www.example.com
Accept-Language: en, mi

요청에는 Content-Length 도 Transfer-Encoding 도 없습니다. 이 요청은 빈 줄에서 끝납니다. 본문이 없습니다. 다음은 응답입니다.

HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

Hello World! My content includes a trailing CRLF.

응답에는 Transfer-Encoding 이 없습니다. Content-Length: 51 이 있으므로 받는 쪽은 빈 줄 뒤 51 바이트를 읽은 뒤 본문을 끝냅니다. 본문 글자는 49 바이트입니다. 본문 끝의 CRLF 2 바이트도 길이에 들어갑니다.

전송 인코딩 값 한 줄

Transfer-Encoding: gzip, chunked

본문을 gzip 으로 압축한 다음 청크로 나눴다는 뜻입니다. 적힌 순서가 입힌 순서입니다. 청크가 맨 뒤에 있으므로 받는 쪽은 크기 0 인 청크까지 읽어 본문의 끝을 찾습니다.

청크로 나눈 본문의 바이트 실물은 RFC 9112 원문에도 RFC 9110 원문에도 실려 있지 않습니다.

실패

이 절은 받는 쪽이 규칙에 어긋난 메시지를 만났을 때 무엇을 돌려주고 연결을 어떻게 하는지 정리합니다. 대상은 요청을 받는 서버, 응답을 받는 프록시와 클라이언트입니다. 같은 400 이라도 강도가 다릅니다.

조건 받는 쪽의 처리 강도
요청이 메시지 문법에 안 맞는다 서버가 400 을 보내고 연결을 닫는다 SHOULD
요청줄이 잘못됐다. 요청 대상에 빈칸이 든 경우가 그렇다 400, 또는 요청 대상을 바르게 인코딩한 주소로 301. 혼자 고쳐서 그냥 처리하지는 않는다 SHOULD · SHOULD NOT
요청 대상이 서버가 다루려는 어떤 URI(Uniform Resource Identifier, 자원 식별자)보다 길다 414 MUST
Host 필드가 없거나, 둘 이상이거나, 값이 틀렸다 400 MUST
요청의 필드 이름과 콜론 사이에 빈칸이 있다 400 으로 거절한다. 응답에서 만난 프록시는 그 빈칸을 지운 뒤 넘긴다 MUST
요청 필드 값이 옛 줄 접기(obs-fold)로 여러 줄에 걸쳐 있다 400 으로 거절하거나, 접힌 곳을 빈칸으로 바꿔 처리한다. 응답에서 만난 프록시는 502 로 바꾸거나 빈칸으로 바꾼다 MUST (둘 중 하나)
요청에 Content-Length 와 Transfer-Encoding 이 함께 있다 거절하거나 Transfer-Encoding 만 따라 처리한다. 어느 쪽이든 응답 뒤 연결을 닫는다 MAY · 닫기는 MUST
요청의 Transfer-Encoding 에서 청크가 맨 뒤가 아니다 400 을 보내고 연결을 닫는다 MUST
Transfer-Encoding 없이 온 Content-Length 값이 잘못됐다 요청이면 서버가 400 을 보내고 닫는다. 응답을 받은 프록시는 서버 쪽 연결을 닫고 응답을 버린 뒤 클라이언트에 502 를 보낸다. 브라우저 같은 사용자 에이전트는 연결을 닫고 응답을 버린다 MUST
보낸 요청이 없는 연결로 데이터가 온다 클라이언트는 그 데이터를 응답으로 보지 않는다. 연결은 닫는다. CRLF 만 온 경우는 버리면 된다 MUST NOT · 닫기는 SHOULD

옛 줄 접기는 필드 값이 이어질 줄 앞에 빈칸이나 탭을 하나 이상 넣어 여러 줄로 이어 쓰던 방식입니다.

Content-Length 값이 잘못됐다는 판정에는 빠져나갈 길이 하나 있습니다. 값이 쉼표로 나뉜 목록일 때입니다. 목록의 값이 모두 올바르고 서로 같으면 그 값 하나를 Content-Length 로 씁니다.

연결을 닫는 까닭

표의 여러 줄이 응답 뒤 연결을 닫으라고 합니다. 경계를 믿을 수 없는 메시지 뒤에 남은 바이트는 같은 연결의 다음 요청으로 읽힙니다. 연결을 닫으면 그 바이트가 해석될 길이 끊깁니다.

HTTP/1.0 메시지에 Transfer-Encoding 이 있으면 Content-Length 가 있어도 경계가 잘못된 것으로 봅니다. 받은 쪽은 그 메시지를 처리한 뒤 연결을 닫아야 합니다(MUST). 보낸 쪽이 메시지 일부를 버퍼에 남겨 두었을 수 있어서입니다.

덜 받은 메시지

청크로 나눈 본문은 크기 0 인 청크가 안 왔으면 덜 받은 것입니다. Content-Length 가 있는 메시지는 받은 본문이 그 값보다 짧으면 덜 받은 것입니다. 클라이언트는 덜 받은 응답을 덜 받은 것으로 기록해야 합니다(MUST). 서버는 덜 받은 요청에 오류 응답을 보낸 뒤 연결을 닫을 수 있습니다(MAY).

덜 받은 응답이 다 받은 것으로 읽히기도 합니다. 두 필드가 다 없는 응답은 연결이 닫히면 끝납니다. 헤더를 온전히 받았다면 그 응답은 다 받은 것으로 봅니다. 본문 중간에 네트워크가 끊겨 연결이 닫혀도 받는 쪽은 이것을 다 받은 응답과 구별하지 못합니다.

아래 연결이 오류를 알린 경우만 덜 받은 것으로 갈립니다. TLS(Transport Layer Security, 전송 계층 보안)에서 불완전한 종료가 나면 응답은 덜 받은 것이 됩니다. 서버에게 길이나 청크로 끝을 적기를 권하는 것도 이 구별이 안 되기 때문입니다.

헤더 중간에서 응답이 끊길 수도 있습니다. 상태 코드의 뜻이 헤더에 기대고 있다면 클라이언트는 그 뜻이 전해졌다고 볼 수 없습니다. 다음에 무엇을 할지 정하려면 요청을 다시 보내야 할 수도 있습니다.

요청 스머글링

요청 스머글링은 받는 장비들이 같은 메시지를 다르게 끊어 읽는 차이를 이용합니다. 겉보기에 멀쩡한 요청 안에 다른 요청을 숨깁니다. 숨긴 요청은 정책으로 막혔을 요청일 수 있습니다.

두 필드가 함께 든 메시지가 그 통로가 됩니다. 옛 구현은 청크로 경계를 정한 메시지에 진행 표시줄용으로 어림한 Content-Length 를 함께 붙이기도 했습니다. 그래서 두 필드가 함께 오면 Transfer-Encoding 이 이기도록 정했습니다. 이 메시지를 넘겨받은 뒷단 장비가 그 규칙을 안 따르면 문제가 생깁니다. HTTP/1.0 만 구현한 장비가 특히 그렇습니다.

sequenceDiagram
    participant 클라이언트
    participant 프록시
    participant 뒷단 서버
    클라이언트->>프록시: 두 필드가 함께 든 요청 하나
    Note over 프록시: Transfer-Encoding 으로 끊어 읽는다
    프록시->>뒷단 서버: Content-Length 를 지우지 않고 넘긴다
    Note over 뒷단 서버: HTTP/1.0 만 알아 Content-Length 로 끊어 읽는다
    Note over 뒷단 서버: 프록시가 본문으로 본 바이트 일부를 다음 요청으로 읽는다

그림의 프록시는 규칙을 어겼습니다. 중계 장비는 이런 메시지를 넘기기 전에 Content-Length 를 지워야 합니다(MUST). 서버는 이런 요청에 응답한 뒤 연결을 닫아야 합니다(MUST).

응답 스플리팅

응답 스플리팅은 두 성질을 함께 노립니다. HTTP/1.1 메시지가 줄 단위로 끊긴다는 성질, 그리고 지속 연결에서 응답이 요청 순서대로 짝지어진다는 성질입니다. 다른 이름은 CRLF 주입입니다. 대개 애플리케이션 서버의 취약점에서 시작합니다.

공격자는 요청의 매개변수 하나에 인코딩한 데이터를 실어 보냅니다. 서버가 그 값을 풀어 응답 헤더 필드 어딘가에 다시 적는다면 어느 매개변수든 통로가 됩니다. 풀린 데이터는 응답이 끝나고 다음 응답이 시작된 모양으로 꾸며져 있습니다. 받는 쪽에는 응답 하나가 둘로 쪼개져 보입니다. 뒤쪽 응답의 내용은 공격자가 정한 것입니다.

sequenceDiagram
    participant 공격자
    participant 중계 장비
    participant 서버
    공격자->>중계 장비: 요청 1 · 매개변수에 인코딩한 데이터
    중계 장비->>서버: 요청 1
    Note over 서버: 데이터를 풀어 응답 헤더 필드에 다시 적는다
    서버-->>중계 장비: 응답 1 · 응답이 둘 든 모양
    공격자->>중계 장비: 같은 지속 연결로 요청 2
    Note over 중계 장비: 쪼개진 뒤쪽 응답을 요청 2 의 답으로 믿는다

공격자는 같은 지속 연결에 다른 요청을 하나 더 보냅니다. 중계 장비를 포함한 받는 쪽은 쪼개진 뒤쪽 응답을 그 요청의 답으로 믿습니다. 요청이 공유 캐시를 거치면 피해가 특히 커질 수 있습니다.

경계

Content-Length 는 RFC 9112 가 정한 필드인가. 아닙니다. 필드의 뜻은 RFC 9110 이 정합니다. RFC 9112 에 드는 것은 그 값으로 HTTP/1.1 본문의 끝을 찾는 규칙입니다.

뜻부터 보면 Content-Length 는 데이터 길이를 음이 아닌 십진수 바이트 수로 알리는 필드입니다. 본문을 싣는 메시지에서는 이 값이 실린 데이터의 양입니다. 이 값으로 메시지의 경계를 가를 수 있습니다.

본문을 싣지 않는 메시지에서도 이 필드에는 뜻이 있습니다. 응답으로 고른 데이터의 지금 길이를 가리킵니다. 받는 쪽은 이 값으로 전송 시간을 어림하거나 전에 저장해 둔 것과 견줄 수 있습니다. 이 쓰임은 경계를 가르는 일과 상관이 없습니다.

RFC 9112 의 Content-Length 소절은 뜻을 RFC 9110 에 맡깁니다. 그리고 이 값으로 HTTP/1.1 본문의 끝을 찾는 쓰임만 다룹니다. Transfer-Encoding 이 든 메시지에 Content-Length 를 보내면 안 된다는 요구(MUST NOT)도 이 소절에 있습니다.

RFC 9110 의 정의는 경계를 가르는 쓰임의 예로 RFC 9112 의 이 소절을 가리킵니다. 두 문서가 서로를 가리키며 선을 긋습니다.

선을 가르는 원리도 RFC 9110 에 있습니다. HTTP 는 주 판마다 메시지의 시작과 끝을 가리는 장치를 따로 정합니다. 이 장치가 프레이밍입니다. 같은 뜻이 HTTP/2 와 HTTP/3 에서는 다른 프레이밍에 실립니다.

flowchart TD
    S["RFC 9110 · 요청과 응답의 뜻 · Content-Length 의 뜻"]
    S --> H1["RFC 9112 · HTTP/1.1 프레이밍 · Content-Length 로 본문 끝을 찾는다"]
    S --> H2["RFC 9113 · HTTP/2 · 같은 뜻을 다른 프레이밍에 싣는다"]
    S --> H3["RFC 9114 · HTTP/3 · 같은 뜻을 QUIC 위에 싣는다"]

관련 항목

HTTP 를 함께 정의하는 표준 문서

RFC 9110 · RFC 9111 · RFC 9113 · RFC 9114

이 문서와 폐기·갱신으로 이어진 표준 문서

RFC 7230 · RFC 9931 · RFC 2616

이 문서를 펴내고 번호를 매기는 기구와 체계

RFC · IETF · STD · IANA · RFC Editor

요구 강도를 정하는 낱말과 문서

MUST · SHOULD · MAY · BCP 14 · RFC 2119 · RFC 8174

이 문서가 바이트 모양을 정하는 프로토콜

HTTP · HTTP/1.1 · HTTP/1.0

메시지를 이루는 구성 요소

시작줄 · 요청줄 · 상태줄 · 헤더 · 필드 · 메시지 본문 · 트레일러 · CRLF · 사유 문구 · 요청 대상

본문 길이와 연결 관리에 쓰이는 헤더 필드

Content-Length · Transfer-Encoding · Connection · Host · TE · Upgrade

본문을 조각으로 나눠 보내는 방식

청크 전송 인코딩 · 전송 인코딩 · 콘텐츠 인코딩 · gzip · 스트리밍 응답

연결을 다시 쓰는 방식과 응답 순서가 낳는 막힘

지속 연결 · HTTP 파이프라이닝 · Keep-Alive · 커넥션 풀 · 헤드 오브 라인 블로킹

파이프라이닝과 재전송을 가르는 메서드 성질

멱등성 · 안전한 메서드 · 요청 메서드

메시지를 잘못 끊어 읽을 때 터지는 공격

HTTP 요청 스머글링 · HTTP 응답 스플리팅 · CRLF 주입 · 캐시 포이즈닝

이 문서가 거절할 때 쓰는 상태 코드

상태 코드 · HTTP 400 · HTTP 411 · HTTP 414 · HTTP 501 · HTTP 502

이 메시지를 중간에서 다시 읽는 중계 장비

프록시 · 리버스 프록시 · 게이트웨이 · 로드 밸런서 · CDN

같은 뜻을 다른 프레이밍으로 나르는 프로토콜

HTTP/2 · HTTP/3 · QUIC · 프레이밍

연결을 다른 프로토콜로 넘기는 장치

CONNECT · 터널 · 웹소켓 · 프로토콜 업그레이드

이 메시지가 올라타는 아래 계층 프로토콜

TCP · TLS · HTTPS · 소켓 · 포트

다른 이름: STD 99