2.1강 — HTTP 요청 읽기: 메서드, URI, 요청 헤더¶
이번 회차의 범위
- 왜 HTTP인가: 디스크에서 사라진 흔적이 회선에는 남는다
- HTTP의 성격(글자 기반, 상태 없음)과 버전의 역사. [변경됨] HTTP/1.1·2·3의 현재 표준 문서
- 요청의 네 부분: 요청 줄, 헤더, 빈 줄, 본문
- 메서드: [정정] 서버가 반드시 지원해야 하는 메서드는 GET과 HEAD, "안전"과 "멱등"의 차이
- 요청 대상(URI)과 쿼리 문자열, 퍼센트 인코딩
- 요청 헤더 하나씩: Host, User-Agent, Accept 계열, Cookie, Authorization, X-Forwarded-For, Referer
- [변경됨] 지금 브라우저가 보내는 것: Referer 정책, Client Hints,
Sec-Fetch-*(직접 수집한 Chromium 요청으로 확인)
실습 환경과 표기
**(직접 실행 결과)**는 이 작업 환경에서 얻었습니다. 브라우저 요청은 내장 Chromium(헤드리스, 버전 153.0.8010.12)을 Playwright로 움직여 루프백의 연습용 웹 서버(파이썬 http.server)에 실제로 보낸 것이고, 서버가 받은 헤더를 그대로 기록하는 동시에 tcpdump로 수집했습니다. 명령줄 도구의 요청은 curl 8.5.0으로 보냈습니다. 사례에 나오는 사람과 계정은 가상이며, 외부 주소는 문서용 대역만 씁니다.
1. 디스크에는 없는데 회선에는 있다¶
(가상 사례) 1.4강의 연구 기관에서 또 한 명의 직원이 조사 대상이 됐습니다. 그런데 이번에는 노트북이 전체 디스크 암호화로 잠겨 있고, 직원은 브라우저를 비공개 모드로만 썼다고 합니다. USB에 넣어 다니는 휴대용 브라우저를 썼을 가능성도 있고요. 디스크 포렌식으로 웹 활동을 복원하기는 어려워 보입니다.
이럴 때 남는 것이 그 활동이 지나간 길입니다(1.1강). 웹 활동은 거의 모두 HTTP로 오가고, 그 기록은 프록시 로그(1.3강), 전체 패킷(1.6강), 그리고 다음 회차들에서 볼 웹 서버 로그와 Zeek 로그에 남습니다. 조사에서 HTTP를 읽을 일은 거의 피할 수 없어요.
- 사람의 웹 활동: 어떤 페이지를 봤고 무엇을 내려받았나. 직원의 비위 조사, 형사 사건에서 자주 묻습니다.
- 프로그램끼리의 대화(API): 요즘 HTTP는 사람이 보는 웹 페이지만큼이나 프로그램이 다른 서비스와 데이터를 주고받는 API의 통로입니다. 어떤 소프트웨어가 어떤 API를 썼는지 알면 그 시스템에서 무슨 일이 있었는지 꽤 자세히 그릴 수 있어요.
- 악성코드의 명령 채널(C2): 많은 악성코드가 HTTP나 HTTPS로 주인과 통신합니다. 악성코드 분석가와 함께 보면, 어떤 명령이 오갔고 무엇이 빠져나갔는지를 알 수 있습니다.
- 웹 서버 침해: 서버 로그와 디스크 분석에 더해 트래픽 자체를 보면, 공격자가 흔적을 지웠더라도 더 온전한 그림이 나옵니다.
2. HTTP는 어떤 프로토콜인가¶
2.1 글자로 된 편지, 서로 모르는 편지¶
HTTP는 요청과 응답을 주고받는 프로토콜입니다. 편지에 비유하면 이해가 쉬워요. 클라이언트가 "이 문서를 보내 주세요"라는 편지(요청)를 쓰면, 서버가 "여기 있습니다" 하고 답장(응답)을 보냅니다.
HTTP/1.x의 편지에는 두 가지 성질이 있습니다.
- 글자로 씁니다. 요청과 응답의 머리 부분이 사람이 읽을 수 있는 영문자와 숫자로 되어 있어요. 1.9강에서 스트림 따라가기로 본 그대로입니다. 그래서 도구 없이도 꽤 많은 걸 읽을 수 있습니다.
- 편지끼리 서로 모릅니다(상태 없음, stateless). 각 요청과 응답은 그 자체로 완결되고, 앞에서 무슨 요청이 있었는지 프로토콜 차원에서는 기억하지 않습니다. 그래서 "로그인한 사람"처럼 이어지는 상태가 필요하면 쿠키 같은 별도 장치를 써야 해요(5.4절).
비유와 다른 점도 있습니다. 실제로는 웹 페이지 하나를 여는 데 편지가 한 통이 아니라 수십에서 수백 통 오갑니다. 페이지 본문, 이미지, 스타일 시트, 스크립트가 각각 따로 요청되고, 여러 서버에서 옵니다. 그래서 "그 사람이 이 사이트에 접속했다"는 한 문장 뒤에는 요청 수백 줄이 있고, 그중 무엇이 사람이 직접 한 일인지 가려내는 것이 조사자의 일입니다(5.7절, 2.4강).
전통적으로 HTTP는 TCP 80번 포트, HTTPS는 TCP 443번을 씁니다. 하지만 1.9강과 1.12강에서 봤듯이 포트 번호는 약속일 뿐이고, HTTP는 어떤 포트로도 오갈 수 있습니다.
2.2 [변경됨] 버전의 역사와 지금의 표준 문서¶
| 버전 | 처음 나온 해 | 특징 | 지금의 표준 문서 |
|---|---|---|---|
| HTTP/0.9 | 1991 | 요청 줄 한 줄, 헤더 없음 | 사실상 보이면 안 됨 |
| HTTP/1.0 | 1996 | 헤더와 상태 코드 등장 | RFC 1945(정보 문서) |
| HTTP/1.1 | 1997 | Host 필수, 연결 재사용 |
RFC 9110(의미), RFC 9112(메시지 형식), 2022 |
| HTTP/2 | 2015 | 이진 형식, 다중화, 헤더 압축 | RFC 9113, 2022(RFC 7540 대체) |
| HTTP/3 | 2022 | QUIC(UDP) 위에서 동작, TLS 필수 | RFC 9114, 2022 |
교재는 HTTP/1.1의 기준으로 RFC 2616을 인용하는데, 이 문서는 2014년 RFC 7230~7235로, 다시 2022년 RFC 9110~9112로 대체됐습니다. 또 교재는 HTTP/3를 "2016년"으로 적고 "아직 개발 중"이라고 설명하지만, HTTP/3는 2022년 6월 RFC 9114로 표준이 됐고, 그 바탕인 QUIC는 2021년 RFC 9000입니다. 1.2강에서 본 대로 HTTP/3(QUIC)는 UDP 443번을 쓰기 때문에 프록시를 비껴갈 수 있다는 점이 조사에서 중요합니다. HTTP/2와 HTTP/3는 2.3강에서 따로 다룹니다. 이번 회차와 다음 회차는 HTTP/1.1 기준입니다. 버전이 바뀌어도 메서드, 상태 코드, 헤더의 뜻은 같기 때문에, 여기서 익힌 것은 그대로 이어집니다.
3. 요청의 네 부분¶
실제 요청 하나를 놓고 봅니다. 내장 Chromium으로 연습용 서버의 /start 페이지를 열고, 그 안의 링크를 눌렀을 때 서버가 받은 요청입니다.
(직접 실행 결과, 서버가 받은 그대로)
GET /next?q=orchid+results HTTP/1.1
Host: 127.0.0.1:8201
Connection: keep-alive
sec-ch-ua: "HeadlessChrome";v="153", "Not_A Brand";v="8", "Chromium";v="153"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/153.0.8010.12 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:8201/start
Accept-Encoding: gzip, deflate, br, zstd
flowchart TD
A["요청 줄<br/>메서드 · 요청 대상 · 버전"] --> B["헤더<br/>이름: 값 (한 줄에 하나)"]
B --> C["빈 줄<br/>헤더의 끝"]
C --> D["본문<br/>POST 등에서만"]
그림 읽는 법: 위에서 아래로, 요청이 회선 위에 놓이는 순서입니다. 줄 끝은 모두 \r\n이고, 헤더가 끝났다는 표시는 빈 줄 하나입니다. 1.13강에서 본문을 잘라 낼 때 \r\n\r\n을 찾은 이유가 이것이에요.
- 요청 줄(필수):
GET(메서드),/next?q=orchid+results(요청 대상),HTTP/1.1(버전). 공백 하나로 구분합니다. - 헤더:
이름: 값형식이 한 줄에 하나씩. HTTP/1.1에서 반드시 있어야 하는 건Host뿐이고, 나머지는 대부분 선택입니다. 즉, 보내는 쪽이 마음대로 넣거나 빼거나 꾸밀 수 있어요. 이 점을 이번 회차 내내 기억하세요. - 빈 줄: 헤더의 끝.
- 본문:
GET요청에는 보통 없습니다.POST나PUT처럼 데이터를 보내는 요청에 들어가요(1.13강의 멀티파트 본문).
4. 메서드: 무엇을 해 달라는 요청인가¶
4.1 자주 보는 메서드¶
| 메서드 | 뜻 | 조사에서 볼 점 |
|---|---|---|
GET |
자원을 달라 | 가장 흔함. 쿼리 문자열로 값도 보냄 |
POST |
데이터를 보낼 테니 처리해 달라 | 폼 제출, 업로드, API 호출. 내용은 본문에(로그에는 보통 없음, 1.4강) |
HEAD |
GET과 같되 헤더만 달라 |
파일이 바뀌었는지, 크기가 얼마인지 확인. 정찰 도구도 씀 |
PUT |
이 위치에 이 내용으로 만들어(바꿔) 달라 | 서버에 파일을 올리는 데 쓰일 수 있음. 웹 서버에서 보이면 주목 |
DELETE |
이 위치의 자원을 지워 달라 | API에서는 정상, 정적 웹 서버라면 이상 |
OPTIONS |
무엇을 지원하는지 알려 달라 | 브라우저의 사전 확인(CORS) 요청, 스캐너 |
TRACE |
받은 요청을 그대로 돌려 달라 | 진단용. 대부분 꺼져 있음. 보이면 스캔 의심 |
CONNECT |
터널을 열어 달라 | 프록시에서 HTTPS를 중계할 때(1.2강) |
WebDAV처럼 HTTP를 확장한 프로토콜은 PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK 같은 자기 메서드를 씁니다. 파일 공유처럼 쓰는 WebDAV는 데이터 유출 통로가 되기도 하니, 낯선 메서드가 보이면 무슨 프로토콜인지부터 확인하세요.
[정정] 서버가 반드시 지원해야 하는 메서드는 GET과 HEAD
교재는 "서버가 반드시 지원해야 하는 메서드는 GET과 POST"라고 설명합니다. 현재 표준인 RFC 9110 9.1절은 "모든 범용 서버는 GET과 HEAD를 지원해야 하며, 나머지는 모두 선택"이라고 정합니다(그 이전의 RFC 7231도 같았습니다). 실제로 POST를 받지 않는 정적 웹 서버도 많아요. 조사에서 이 차이가 중요한 순간은 드물지만, 서버가 HEAD에 이상하게 반응한다면 그 자체가 단서가 될 수 있습니다.
4.2 [정정] "안전"과 "멱등"은 다르다¶
교재는 GET이 "멱등(idempotent)", 즉 나중에 같은 요청을 보내도 같은 결과가 나오고 서버의 내용을 바꾸지 않는다고 설명합니다. 표준은 이것을 두 개념으로 나눕니다.
- 안전(safe): 요청이 서버의 상태를 바꾸려는 의도가 없다.
GET,HEAD,OPTIONS,TRACE. - 멱등(idempotent): 같은 요청을 여러 번 보내도 한 번 보낸 것과 결과가 같다. 안전한 메서드 전부와
PUT,DELETE. 같은 파일을 두 번PUT해도, 같은 자원을 두 번DELETE해도 결과는 같으니까요. POST는 둘 다 아닙니다. 같은 주문을 두 번POST하면 주문이 두 건이 될 수 있어요.
조사에서의 의미: 이건 약속일 뿐입니다. 교재도 지적하듯, 실제로는 GET /delete.php?id=17처럼 GET으로 상태를 바꾸는 웹 앱이 흔하고, 공격자는 일부러 GET으로 명령을 보내기도 합니다(2.5강에서 볼 웹 서버 로그의 공격 흔적이 대부분 GET이에요). 그러니 "GET이니까 아무것도 안 바뀌었다"고 결론 내리면 안 됩니다. 메서드는 의도를 짐작하는 출발점이고, 실제 효과는 응답과 서버 쪽 기록으로 확인합니다.
5. 요청 대상(URI)과 쿼리 문자열¶
요청 줄의 가운데는 클라이언트가 원하는 자원을 가리키는 요청 대상입니다. 흔히 URI라고 부르고, 로그에서는 "요청 문자열", "URL"이라고도 합니다.
- 경로: 예전에는 서버 디스크의 폴더와 파일 이름을 그대로 가리켰지만, 지금은 웹 앱이 경로를 해석해서 내용을 만들어 내는 경우가 대부분입니다.
/questions/34476646/forensic-...처럼 폴더처럼 보여도 그런 폴더가 서버에 실제로 있는 건 아니에요. - 쿼리 문자열:
?뒤에이름=값을&로 이어 붙입니다.GET요청이 서버에 값을 넘기는 방법이에요. 검색어, 페이지 번호, 추적용 식별자가 여기 들어갑니다. - 퍼센트 인코딩: 주소에 쓸 수 없는 글자는
%뒤 두 자리 16진수로 바꿉니다. 공백은%20,/는%2F, 한글 "가"는 UTF-8 세 바이트라%EA%B0%80가 돼요. 폼에서 온 쿼리에서는 공백이+로 바뀌기도 합니다. 위 요청의orchid+results가 그 예입니다(1.13강 문제 2).
[보충] 로그에 남는 URI는 \"사람이 읽은 것\"이 아닐 수 있다
로그와 패킷에는 인코딩된 그대로(%EA%B0%80)가 남습니다. 검색어를 찾을 때는 디코딩해서 읽어야 하고, 반대로 키워드로 검색할 때는 인코딩된 모양으로도 찾아야 합니다. 같은 글자가 대문자 16진수(%2F)와 소문자(%2f)로 섞여 있을 수 있다는 점도 기억하세요. 공격자는 일부러 이중 인코딩(%252F)을 써서 필터를 피하기도 합니다.
6. 요청 헤더 하나씩 읽기¶
6.1 Host¶
HTTP/1.1 요청에서 유일하게 필수인 헤더입니다. 한 서버(IP 주소 하나)가 이름이 다른 여러 사이트를 함께 운영하는 가상 호스트 환경에서, 어느 사이트에 대한 요청인지를 알려 줍니다. 그래서 IP 주소만 보고 "어느 사이트에 갔다"고 말할 수 없어요. 같은 IP에 수백 개의 사이트가 있을 수 있고(CDN이 대표적), 어느 사이트였는지는 Host(HTTPS라면 SNI, 1.2강)가 알려 줍니다. HTTP/2와 HTTP/3에서는 같은 역할을 :authority라는 의사 헤더가 합니다(2.3강).
6.2 User-Agent: 스스로 밝힌 이름¶
요청을 보낸 소프트웨어가 스스로 밝히는 이름입니다. 브라우저 종류와 버전, 운영체제가 들어가는 경우가 많아 사용자의 환경을 그리는 데 좋은 재료예요. 하지만 세 가지를 꼭 기억하세요.
- 마음대로 바꿀 수 있습니다. 표준상 선택 헤더이고, 브라우저 확장 기능이나 명령줄 옵션(
curl -A) 하나로 무엇이든 넣을 수 있습니다. 1.13강의 업로드가 Firefox라고 밝혔지만 실제로는curl이었던 것처럼요. Mozilla/5.0은 아무 뜻이 없습니다. 옛날 웹 사이트들이 이 글자를 보고 기능을 정하던 시절의 호환용 흔적이라, Chrome, Safari, Edge, Firefox가 모두 이렇게 시작합니다.- 기본값 자체도 단서입니다. 이번 실습에서
curl이 보낸 요청은 헤더가 셋뿐이었습니다.
(직접 실행 결과)
같은 서버에 Chromium이 보낸 요청(3절)은 헤더가 열네 개(Referer 포함)였어요. 이렇게 헤더의 개수와 순서, 조합은 User-Agent 문자열보다 꾸미기가 번거로워, 사람의 브라우저인지 자동화 도구인지 가늠하는 보조 단서가 됩니다(2.4강). 그리고 이번 실습의 헤드리스 Chromium은 User-Agent에 HeadlessChrome이라는 글자를 그대로 넣었습니다**(직접 실행 결과)**. 자동화 도구가 이 부분을 고치지 않았다면 바로 드러나는 흔적이에요.
[변경됨] User-Agent는 줄어들고, Client Hints가 생겼다
교재 시절의 User-Agent에는 운영체제의 세부 버전과 브라우저의 전체 버전이 들어 있었습니다. Chromium 계열 브라우저는 2022년(Chrome 101)부터 User-Agent 줄이기를 단계적으로 적용해, 부 버전 번호를 0.0.0으로 고정하고 운영체제 버전도 고정값으로 보냅니다. 대신 더 자세한 정보는 서버가 요청할 때만 보내는 Client Hints(Sec-CH-UA-* 헤더)로 옮겼어요. 기본으로는 위 요청의 sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform 세 가지만 갑니다. 그래서 지금은 "User-Agent에 Windows 10이라고 적혀 있으니 Windows 10이다"라고 읽으면 틀릴 수 있습니다(Windows 11도 같은 값을 보낼 수 있어요). 참고로 이번 실습의 헤드리스 Chromium은 전체 버전(153.0.8010.12)을 그대로 보냈습니다. 일반 데스크톱 브라우저와 다르게 동작했을 수 있으니, 실제 사건에서는 그 버전의 그 브라우저로 직접 시험해 보고 해석하세요.
6.3 Accept, Accept-Language, Accept-Encoding¶
클라이언트가 어떤 응답을 받고 싶은지를 알립니다. Accept는 내용 형식(MIME 유형), Accept-Language는 언어, Accept-Encoding은 압축 방식이에요. 값 뒤의 q=0.9는 선호도(0~1)입니다.
조사에서는 이것도 프로필의 재료입니다. 예를 들어 Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8이면 한국어를 먼저 원하는 사용자 환경이겠죠. 이번 실습의 헤드리스 Chromium은 Accept-Language를 아예 보내지 않았고, Accept-Encoding에는 교재 시절에는 없던 zstd가 들어 있었습니다**(직접 실행 결과)**. 같은 PC에서 갑자기 다른 조합의 요청이 나타난다면, 다른 프로그램이 HTTP를 쓰기 시작했다는 신호일 수 있어요.
6.4 Cookie: 상태 없는 프로토콜에 붙인 기억¶
2.1절에서 말했듯 HTTP 자체는 앞의 요청을 기억하지 못합니다. 그래서 서버는 응답의 Set-Cookie로 작은 값을 클라이언트에 맡겨 두고, 클라이언트는 다음 요청마다 그 값을 Cookie 헤더로 돌려줍니다. 로그인 상태, 장바구니, 사용자 식별이 이렇게 유지돼요.
- 세션 쿠키: 만료 시각이 없어 브라우저를 닫으면 사라집니다.
- 지속 쿠키: 정해진 만료 시각까지 디스크에 남습니다.
조사에서 쿠키는 같은 사람(정확히는 같은 브라우저 저장소) 을 여러 요청, 여러 날에 걸쳐 잇는 끈이 됩니다. 주소가 바뀌어도 같은 쿠키 값이 계속 보이면 같은 브라우저일 가능성이 높아요. 광고·분석 업체의 추적 쿠키는 처음 방문한 시각 같은 과거 정보까지 담고 있어서, 2.3강에서 따로 다룹니다. 반대로 세션 쿠키는 그 자체로 로그인 열쇠라는 점도 잊지 마세요. pcap이나 로그에 남은 세션 쿠키를 함부로 다루면 남의 계정에 들어가는 것과 같습니다.
6.5 Authorization과 Proxy-Authorization: Base64는 암호가 아니다¶
브라우저가 띄우는 로그인 창(웹 페이지 안의 로그인 폼이 아니라)으로 인증하는 방식을 HTTP 인증이라고 하고, 그중 가장 단순한 Basic 방식은 아이디:비밀번호를 Base64로 바꿔 헤더에 넣습니다. 이번 실습에서 curl -u "jdoe:Spring2026!"로 보낸 요청은 이렇게 갔습니다.
(직접 실행 결과)
(직접 실행 결과) jdoe:Spring2026!
Base64는 암호화가 아니라 표기 방식이라, 누구나 바로 되돌릴 수 있습니다(1.13강). 그리고 Basic 인증은 요청마다 이 헤더를 보내므로, 평문 HTTP였다면 그 사이트를 쓰는 동안의 어느 패킷에서든 계정이 보입니다. 조사에서는 사용자의 신원을 밝히는 강한 단서이고, 동시에 매우 민감한 정보이니 보고서에는 가려서 적습니다.
Digest방식은 비밀번호를 그대로 보내지 않지만, 지금은 거의 쓰이지 않습니다.- [보충]
Bearer토큰: 요즘 API는Authorization: Bearer eyJ...처럼 로그인 과정에서 받은 토큰을 보내는 방식이 흔합니다. 토큰이 JWT라면 가운데 부분을 Base64로 풀어 발급자, 사용자, 만료 시각을 읽을 수 있어요. 토큰도 세션 쿠키처럼 그 자체로 열쇠입니다. Proxy-Authorization: 인증이 필요한 프록시를 쓸 때 클라이언트와 첫 프록시 사이에만 오가는 같은 형식의 헤더입니다. 1.3강의 프록시 로그에 사용자 이름이 남는 경우가 이것이에요.
6.6 X-Forwarded-For: 누구나 쓸 수 있는 "원래 주소"¶
프록시나 부하 분산 장비가 요청을 대신 전달할 때, 원래 클라이언트의 주소를 알려 주려고 붙이는 헤더입니다. 여러 단계를 거치면 원래 클라이언트, 첫 프록시, ... 순서로 주소가 쌓여요.
문제는 클라이언트가 처음부터 직접 넣을 수 있다는 것입니다. 이번 실습에서 curl -H "X-Forwarded-For: 10.10.4.3"으로 보내자, 서버는 이 헤더를 그대로 받았습니다. 실제로 연결을 맺은 주소는 127.0.0.1이었는데도요**(직접 실행 결과)**.
10.10.4.3은 1.1강의 사내 프록시 주소입니다. 만약 웹 앱이 "사내 프록시에서 온 요청은 관리 화면 허용"처럼 이 헤더를 믿는다면, 공격자는 헤더 한 줄로 그 통제를 넘을 수 있습니다. 교재가 경고하는 그대로예요.
조사에서의 원칙: 로그에서 X-Forwarded-For 값을 볼 때는 누가 붙였는지를 따져야 합니다. 우리 쪽 프록시나 부하 분산 장비가 붙인 값(맨 오른쪽, 우리 장비와 가장 가까운 값)은 믿을 만하지만, 그 왼쪽에 이미 있던 값은 클라이언트가 적은 것일 수 있습니다. 그리고 그 장비가 클라이언트가 보낸 값을 지우고 다시 쓰는지, 뒤에 덧붙이는지는 설정마다 다르니 확인합니다.
[보충] X-로 시작하는 헤더와 표준 Forwarded
X-는 표준이 아닌 확장 헤더라는 뜻으로 쓰던 관례였지만, 2012년 RFC 6648이 새 헤더에 이 접두어를 붙이지 말라고 권고했습니다. 이미 널리 쓰이던 X-Forwarded-For는 사실상 표준으로 남았고, 같은 목적의 정식 표준 헤더로 Forwarded(RFC 7239, 2014)가 있습니다. 로그에서 둘 다 볼 수 있어요. 그 밖의 X- 헤더는 프로그램마다 제멋대로라, 특이한 X- 헤더가 특정 프로그램의 지문이 되기도 합니다.
6.7 Referer: 어디서 왔나, 그리고 [변경됨] 지금은 얼마나 알려 주나¶
Referer(표준 문서의 철자 실수가 그대로 굳은 이름입니다. 영어 단어는 referrer)는 이 요청을 하게 만든 이전 페이지의 주소입니다. 링크를 눌렀다면 그 링크가 있던 페이지, 이미지나 스크립트라면 그것을 불러온 페이지예요. 그래서 요청들을 Referer로 이으면 사용자가 어떤 길로 페이지를 옮겨 다녔는지 재구성할 수 있습니다. 물론 이것도 선택 헤더라 빠지거나 꾸며질 수 있습니다.
교재는 "HTTPS에서 HTTP로 갈 때는 보내지 않아야 하지만, 실제로는 잘린 값이 가기도 한다"고 설명합니다. 지금 브라우저는 여기에 더해 Referrer-Policy라는 규칙을 따르고, 별도 지정이 없을 때의 기본값은 strict-origin-when-cross-origin입니다(Chrome은 2020년, Firefox는 2021년 무렵부터, 1.3강). 세 가지 경우를 직접 시험했습니다.
(직접 실행 결과)
| 이동 | 서버가 받은 Referer |
|---|---|
같은 출처: http://127.0.0.1:8201/start → /next?q=... |
http://127.0.0.1:8201/start (경로까지 전부) |
다른 출처: http://127.0.0.1:8201/start → http://localhost:8202/landing |
http://127.0.0.1:8201/ (출처만, 경로 없음) |
HTTPS → HTTP: https://127.0.0.1:8443/secure → http://127.0.0.1:8202/plain |
없음 |
"출처(origin)"는 방식, 이름, 포트의 묶음입니다. 127.0.0.1과 localhost는 같은 컴퓨터지만 이름이 달라 서로 다른 출처로 취급됐어요. 정리하면, 지금의 기본 정책에서는
- 같은 사이트 안에서는 이전 페이지의 전체 주소(쿼리 포함)가 갑니다.
- 다른 사이트로 갈 때는 출처만 갑니다. 교재의 예처럼 검색 엔진에서 들어왔다면
Referer: https://검색엔진/까지만 보이고, 무슨 검색어였는지는 보이지 않습니다. - HTTPS에서 HTTP로 내려갈 때는 아예 보내지 않습니다. RFC 9110 10.1.3절의 요구와 같아요.
그러니 Referer가 없다고 "주소를 직접 쳐서 들어왔다"고 단정하면 안 됩니다. HTTPS 페이지의 링크였을 수도, 페이지가 정책을 no-referrer로 정했을 수도 있으니까요.
[보충] Sec-Fetch-*: 브라우저가 알려 주는 \"요청의 사정\"
지금의 브라우저는 Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-User, Sec-Fetch-Dest라는 헤더로 요청이 어떤 상황에서 생겼는지 알려 줍니다(W3C Fetch Metadata). 이번 실습에서 Sec-Fetch-Site는 주소를 직접 열었을 때 none, 같은 출처 링크에서 same-origin, 다른 출처 링크에서 cross-site였고, 링크를 눌러서 이동할 때는 Sec-Fetch-User: ?1이 붙었습니다**(직접 실행 결과)**. 페이지 본문(Sec-Fetch-Dest: document)인지, 이미지나 스크립트인지도 구별돼요. Referer가 줄어든 지금, 이 헤더들은 "사람이 클릭한 탐색인가, 페이지가 알아서 불러온 자원인가"를 가르는 좋은 보조 단서입니다. 단, 이것도 브라우저가 붙이는 값이라 브라우저가 아닌 도구는 보내지 않거나 꾸며서 보낼 수 있습니다.
[보충] 교재 예시의 DNT: 1
"추적하지 마세요"라는 뜻의 DNT 헤더는 사이트들이 따르지 않으면서 사실상 사라졌고, MDN 문서도 폐지됨(deprecated)으로 표시합니다. 오래된 로그에서 보이더라도 큰 의미를 두지 않습니다.
7. 실제 조사에서는 이렇게 씁니다¶
- 요청 줄부터 본다. 메서드, 요청 대상, 버전. 정적 사이트에
PUT이나DELETE, 진단용TRACE, 낯선 WebDAV 메서드가 보이면 주목합니다. - 쿼리 문자열을 디코딩해서 읽는다. 검색어와 식별자가 여기 있습니다. 키워드 검색은 인코딩된 모양으로도 합니다.
Host로 사이트를, IP로는 서버를 말한다. 같은 IP의 다른 사이트를 섞지 않습니다.- 선택 헤더는 "스스로 밝힌 것"으로 읽는다. User-Agent,
X-Forwarded-For,Referer는 꾸밀 수 있습니다. 누가 붙였는지, 다른 증거와 맞는지 확인합니다. - 헤더의 조합으로 프로필을 만든다. 같은 PC의 평소 조합과 다른 요청이 나타나면, 다른 프로그램일 수 있습니다.
- 열쇠는 가린다.
Authorization, 세션 쿠키, 토큰은 계정 그 자체입니다. 보고서와 공유 자료에서 가리고, 쓸 때는 법적 권한을 확인합니다.
8. 정리¶
- HTTP는 글자로 된, 서로 모르는(상태 없는) 요청과 응답입니다. 디스크에서 사라진 웹 활동도 회선과 로그에는 남습니다.
- [변경됨] HTTP/1.1의 기준 문서는 RFC 2616이 아니라 RFC 9110·9112(2022)이고, HTTP/2는 RFC 9113, HTTP/3는 2022년 RFC 9114로 표준이 됐습니다.
- 요청은 요청 줄, 헤더, 빈 줄, 본문으로 되어 있고, 필수 헤더는
Host뿐입니다. 나머지는 보내는 쪽 마음대로입니다. - [정정] 반드시 지원해야 하는 메서드는 GET과 HEAD입니다. "안전"(상태를 바꿀 의도 없음)과 "멱등"(여러 번 보내도 결과 같음)은 다르고, 둘 다 약속일 뿐 실제 효과는 따로 확인합니다.
- 요청 대상의 쿼리 문자열과 퍼센트 인코딩은 디코딩해서 읽습니다.
- User-Agent는 스스로 밝힌 이름이고, [변경됨] 지금은 줄어들고 Client Hints로 일부가 옮겨 갔습니다. 헤더의 조합(
curl은 셋, 브라우저는 열넷)이 보조 단서가 됩니다. - Basic 인증의 Base64는 바로 풀립니다.
X-Forwarded-For는 누구나 넣을 수 있습니다(직접 확인). - [변경됨] 지금의 기본 Referer 정책에서는 같은 출처면 전체 주소, 다른 출처면 출처만, HTTPS→HTTP면 없음이었습니다(직접 확인).
Sec-Fetch-*가 요청의 사정을 알려 줍니다.
복습 문제¶
문제 1. (구조) 다음 요청에서 요청 줄의 세 부분을 나누고, 필수가 아닌 헤더를 모두 고르세요.
POST /api/v2/sync?device=7f3a HTTP/1.1
Host: files.example.net
User-Agent: SyncAgent/4.2
Content-Type: application/json
Content-Length: 58
정답과 해설
메서드 POST, 요청 대상 /api/v2/sync?device=7f3a(경로 /api/v2/sync와 쿼리 device=7f3a), 버전 HTTP/1.1. 필수 헤더는 Host뿐이라, User-Agent, Content-Type, Content-Length는 모두 필수가 아닙니다. 다만 본문이 있는 요청이라면 본문의 길이를 알려 주는 Content-Length(또는 청크 전송, 1.13강 문제 6)가 있어야 서버가 본문을 제대로 읽을 수 있어요. "필수 헤더가 아니다"와 "그 요청에서 없어도 된다"는 다른 이야기입니다. 이 요청은 58바이트 JSON을 API로 보낸 것이고, SyncAgent/4.2는 스스로 밝힌 이름이니 실제 프로그램은 단말에서 확인합니다.
문제 2. (정정된 내용) 동료가 "이 웹 서버는 HEAD 요청에 405(허용 안 되는 메서드)를 돌려줬다. HEAD는 선택 메서드니까 이상할 것 없다"고 말합니다. 맞나요?
정답과 해설
표준(RFC 9110 9.1절)은 범용 서버가 GET과 HEAD를 반드시 지원해야 한다고 정합니다. 그러니 범용 웹 서버가 HEAD를 거부한다면 표준과 맞지 않는 특이한 동작입니다. 일부러 막아 둔 보안 설정일 수도, 특정 웹 앱이나 장비의 특징일 수도, 공격자가 세운 가짜 서버(C2)의 서툰 구현일 수도 있어요. 2.2강에서 볼 것처럼, 이런 "표준에서 살짝 벗어난 동작"은 서버를 식별하는 지문이 되기도 합니다. 그러니 "이상할 것 없다"가 아니라 "기록해 둘 특징"입니다.
문제 3. (흔한 오해) "로그에 GET /admin/user.php?action=remove&id=42만 있고 POST는 없으니, 이 사용자는 아무것도 바꾸지 않았다." 이 결론의 문제를 설명하세요.
정답과 해설
GET이 "안전"하다는 건 표준이 정한 약속일 뿐, 웹 앱이 그 약속을 지키는지는 별개입니다. 이 요청의 쿼리는 action=remove로, 이름만 봐도 계정 42를 지우는 기능처럼 보여요. 실제로 지워졌는지는 ① 응답 상태 코드와 크기(2.2강), ② 웹 앱과 데이터베이스의 기록, ③ 이후 그 계정의 활동 유무로 확인해야 합니다. 메서드로 의도를 짐작할 수는 있지만, 효과를 단정할 수는 없습니다.
문제 4. (디코딩) 프록시 로그에 GET /search?q=%EC%9C%A0%EC%B6%9C+%EB%B0%A9%EB%B2%95 요청이 있습니다. 검색어는 무엇이고, 이 값을 로그에서 찾으려면 어떤 문자열로 검색해야 하나요?
정답과 해설
%EC%9C%A0은 "유", %EC%B6%9C는 "출", +는 공백, %EB%B0%A9은 "방", %EB%B2%95은 "법"이니 검색어는 **"유출 방법"**입니다(UTF-8 기준. 셸에서는 printf '%b' "$(echo '...' | sed 's/+/ /g; s/%/\\x/g')" 같은 방법이나 파이썬의 urllib.parse.unquote_plus로 풀 수 있습니다). 로그에서 찾을 때는 한글 "유출"로 검색하면 안 걸리고, %EC%9C%A0%EC%B6%9C로 찾아야 합니다. 소문자 16진수(%ec%9c%a0...)로 적힌 기록이 섞여 있을 수 있으니 대소문자를 무시하고 찾는 게 안전합니다.
문제 5. (해석) 6.2절에서 curl의 요청은 헤더가 3개, Chromium은 14개였습니다. 어느 PC에서 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/...이면서 헤더는 Host, User-Agent, Accept: */* 셋뿐인 요청이 5분마다 나갑니다. 무엇을 의심할 수 있고, 무엇은 단정할 수 없나요?
정답과 해설
의심할 수 있는 것: 실제 Chrome 브라우저가 페이지를 열 때는 Accept-Language, Accept-Encoding, sec-ch-ua, Sec-Fetch-* 같은 헤더를 함께 보내는 것이 보통인데, 이 요청은 그 조합이 아닙니다. 그래서 브라우저가 아닌 프로그램이 Chrome의 이름을 빌려 쓴 것일 가능성이 있고, 5분 간격의 규칙성(1.10강)과 함께 보면 자동화된 통신으로 보입니다. 단정할 수 없는 것: 악성코드라는 결론. 업데이트 도구, 모니터링 에이전트, 사내 스크립트도 이렇게 보낼 수 있어요. 목적지, 응답 내용, 단말에서 그 연결을 연 프로세스를 확인해야 합니다. 그리고 브라우저가 이미지나 API를 불러오는 요청(Sec-Fetch-Dest가 document가 아닌 것)은 헤더 조합이 다를 수 있으니, 같은 브라우저의 같은 종류 요청과 비교합니다.
문제 6. (변경된 조건) 2016년 자료를 보고 만든 조사 절차서에 "검색 엔진에서 들어온 방문자는 Referer의 q= 값으로 검색어를 확인한다"고 적혀 있습니다. 지금도 통하나요? 직접 확인한 결과를 근거로 설명하세요.
정답과 해설
거의 통하지 않습니다. 지금 브라우저의 기본 Referrer-Policy인 strict-origin-when-cross-origin에서는 다른 출처로 이동할 때 출처만 보냅니다. 6.7절의 실험에서 127.0.0.1:8201/start에서 localhost:8202로 이동하자 Referer는 http://127.0.0.1:8201/이었고 경로와 쿼리는 빠졌어요. 게다가 검색 엔진은 대부분 HTTPS이고, HTTPS에서 HTTP 사이트로 가면 Referer가 아예 없었습니다. 검색 엔진 쪽에서도 자체 정책으로 검색어를 넘기지 않는 경우가 많습니다. 그러니 검색어는 검색 엔진으로 가는 요청 자체(프록시 로그의 /search?q=, 1.4강의 자동완성 요청)에서 찾아야 합니다.
문제 7. (적용) 웹 서버 로그에 관리자 페이지 접근이 있고, 출발지는 외부 주소인데 X-Forwarded-For: 10.10.4.3, 192.0.2.10이 함께 기록돼 있습니다. 서버 앞에는 우리 조직의 부하 분산 장비가 있고, 그 장비는 받은 값 뒤에 연결한 주소를 덧붙입니다. 실제로 연결한 클라이언트는 누구일 가능성이 크고, 10.10.4.3은 무엇인가요?
정답과 해설
우리 부하 분산 장비가 뒤에 덧붙이는 방식이라면, 맨 오른쪽 192.0.2.10이 장비가 실제로 본 연결 주소, 즉 믿을 수 있는 클라이언트 주소입니다. 그 왼쪽의 10.10.4.3은 장비에 들어오기 전에 이미 헤더에 있던 값이에요. 그건 중간의 다른 프록시가 붙였을 수도 있지만, 외부 클라이언트가 직접 써 넣었을 수도 있습니다. 하필 사내 프록시 주소라는 점은, 웹 앱이 이 헤더로 "사내에서 온 요청"을 판단하는지 시험해 본 흔적일 수 있어요(6.6절). 그러니 ① 웹 앱이 어느 값을 믿는지, ② 그 요청에 대한 응답이 성공이었는지, ③ 192.0.2.10의 다른 활동을 확인합니다.
문제 8. (흔한 오해) "Authorization 헤더의 값은 암호화돼 있어서, pcap을 공유해도 계정이 노출되지는 않는다." 무엇이 틀렸나요?
정답과 해설
Basic 방식의 값은 Base64로 바꾼 것일 뿐이라, 6.5절처럼 base64 -d 한 번이면 아이디:비밀번호가 나옵니다. 평문 HTTP였다면 pcap을 받은 누구나 계정을 읽을 수 있어요. Bearer 토큰이나 세션 쿠키도 그 값 자체가 열쇠라, 유효 기간 안이라면 그대로 다시 써서 들어갈 수 있습니다. 그래서 pcap이나 로그를 공유할 때는 이런 헤더를 가리거나 지운 사본을 만들고, 원본은 접근을 제한합니다. 조사 중 알게 된 계정을 써서 로그인해 보는 것은 법적 권한 없이 해서는 안 됩니다.
문제 9. (해석) 같은 PC의 하루 치 요청 중, 사용자가 실제로 링크를 눌러 연 페이지만 골라내고 싶습니다. 이번 회차에서 본 헤더 중 무엇을 쓸 수 있고, 어떤 한계가 있나요?
정답과 해설
지금 브라우저라면 Sec-Fetch-Dest: document(페이지 본문 요청)와 Sec-Fetch-User: ?1(사용자 활성화로 시작된 탐색)이 함께 있는 요청이 좋은 후보입니다. 6.7절의 실험에서 링크를 눌러 이동한 요청에 이 둘이 붙었어요. Sec-Fetch-Site로 주소를 직접 연 것(none)과 링크로 온 것을 나눌 수도 있습니다. 한계는 ① 이 헤더들이 평문 HTTP나 TLS 가로채기 프록시처럼 헤더를 볼 수 있는 곳에서만 보인다는 것(HTTPS 패킷만 있다면 안 보입니다), ② 기본 프록시 로그 형식에는 헤더가 남지 않는다는 것(1.3강), ③ 오래된 브라우저나 브라우저가 아닌 도구는 보내지 않는다는 것, ④ 꾸밀 수 있다는 것입니다. 헤더가 없다면 2.4강처럼 요청의 시간 간격과 Referer 연결로 사람의 탐색을 추정합니다.
참고 자료¶
- 작업 환경에서 쓴 도구: Chromium 153.0.8010.12(헤드리스, Playwright), curl 8.5.0, 파이썬 3
http.server, tcpdump 4.99.4, tshark 4.2.2. 실행 결과는 2026-11-05에 얻었습니다. - IETF, RFC 9110: HTTP Semantics (2022). 9.1절(GET·HEAD 필수), 9.2.1~9.2.2절(안전한 메서드와 멱등 메서드), 7.2절(Host), 10.1.3절(Referer와 보안 수준이 낮은 요청), 11절(HTTP 인증).
- IETF, RFC 9112: HTTP/1.1, RFC 9113: HTTP/2, RFC 9114: HTTP/3, RFC 9000: QUIC.
- IETF, RFC 3986: URI 일반 문법 2.1절(퍼센트 인코딩), RFC 7617: Basic 인증, RFC 7239: Forwarded, RFC 6648:
X-접두어 사용 중단 권고. - W3C, Referrer Policy와 Fetch Metadata Request Headers.
- Chromium, User-Agent 줄이기 안내 (2026-11-05 검색 결과로 확인: Chrome 101부터 부 버전
0.0.0고정, Client Hints로 이전).
이 페이지에 대하여
여러 학습 자료를 바탕으로 직접 다시 쓴 개인 학습 노트입니다. 실험용 서버와 요청은 직접 만들었고, 사례는 가상입니다.