브라우저는 우리 모르게 무엇을 보내고 있을까
이메일은 어떻게 "읽었다"는 걸 알아낼까
이메일은 어떻게 "읽었다"는 걸 알아낼까
발신자가 수신 여부를 추적하는 가장 일반적인 방법은 트래킹 픽셀(tracking pixel) 입니다. 메일 본문 HTML에 1×1 크기의 투명 이미지를 숨겨 넣는 방식입니다.
<img src="https://tracker.example.com/open.gif?msg_id=abc123&uid=xyz"
width="1" height="1" style="display:none">
수신자가 메일을 열면 메일 클라이언트가 이 이미지를 자동으로 다운로드합니다. 그 순간 발신자 서버에 HTTP 요청이 도달하고, 서버는 msg_id와 uid로 누가 언제 열어봤는지 기록합니다.
원리는 단순하지만, 실제로는 클라이언트마다 동작이 꽤 다릅니다.
추적이 실패하는 경우
- 수신자가 메일 클라이언트에서 이미지 자동 로드를 꺼둔 경우
- 텍스트 모드로만 메일을 본 경우
- 메일이 스팸함으로 분류되어 자동 처리된 경우
여기서 한 가지만 더 짚고 가겠습니다. 메일 클라이언트가 이미지를 요청할 때, 만약 그 도메인에 쿠키가 저장돼 있다면 쿠키도 함께 자동으로 전송됩니다. 이 사실이 뒤에서 다룰 CSRF의 핵심입니다.
잠깐, 메일 원본을 열어보면 더 흥미롭습니다
이론은 그렇고, 실제 메일에서 어떻게 보이는지 직접 들여다보겠습니다. 아래는 네이버 메일에서 Gmail로 보낸 어느 메일의 raw 소스 일부입니다.
Subject: =?utf-8?B?7JWI64WV7ZWY7IS47JqU?=
From: =?utf-8?B?7LWc7KCV7JuQ?= <sender@naver.com>
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64
PGh0bWw+PGhlYWQ+PHN0eWxlPnB7bWFyZ2luLXRvcDow...
처음 보면 두 가지 의문이 듭니다. 한글 제목은 왜 알 수 없는 문자열로 되어 있고, 본문은 왜 의미 없어 보이는 알파벳·숫자 덩어리인가.
Base64는 보안이 아니라 호환성을 위한 것
SMTP는 1982년에 설계된 프로토콜이라 7-bit ASCII만 안전하게 처리합니다. 한글, 이모지, 첨부파일 같은 비ASCII 데이터는 그대로 전송할 수 없습니다. 그래서 MIME 표준에서는 비ASCII 데이터를 8-bit에서 7-bit로 변환하는 방식을 정의했고, 그 대표가 Base64입니다.
헤더의 =?utf-8?B?...?= 형식은 RFC 2047에서 정의한 encoded-word입니다.
=?utf-8?B?7JWI64WV7ZWY7IS47JqU?=
│ │ │
│ │ └── 실제 Base64 데이터
│ └────── B = Base64 (Q는 Quoted-Printable)
└────────── 문자셋
본문 인코딩 방식은 Content-Transfer-Encoding: base64 헤더로 명시됩니다. 메일 클라이언트는 이 헤더를 보고 자동으로 디코딩해서 사용자에게 원래 텍스트로 보여줍니다.
여기서 짚을 점은 Base64가 암호화가 아니라는 사실입니다. 누구나 디코딩할 수 있고, 의도는 그저 "전송 중 깨지지 말 것"입니다. 터미널에서 한 줄이면 풀립니다.
echo "7JWI64WV7ZWY7IS47JqU" | base64 -d
# 안녕하세요
디코딩하면 보이는 추적 장치
위 메일의 HTML 본문을 Base64 디코딩하면 다음 코드가 나옵니다.
<html><body>
<div style="font-size:14px;">안녕하세요</div>
</body></html>
<table style='display:none'>
<tr><td>
<img src="https://mail.naver.com/readReceipt/notify/?img=jYnq...gif"
border="0"/>
</td></tr>
</table>
display:none 처리된 테이블 안에 <img> 태그가 숨어 있습니다. 앞서 이야기한 트래킹 픽셀의 실물입니다.
- 호스트:
mail.naver.com - 경로:
/readReceipt/notify/ - 쿼리:
img=jYnq...→ 어느 메일이 누구에 의해 열렸는지 식별하는 고유 토큰
수신자가 Gmail에서 이 메일을 여는 순간, Gmail이 이미지를 로드 → 네이버 서버에 요청 도달 → 발신자의 "수신확인"에 읽음 표시가 뜨는 구조입니다. 이론으로 설명한 동작이 메일 원본에 그대로 박혀 있는 셈입니다.
본인이 받은 메일이 의심스럽다면 Gmail에서 "원본 보기(Show original)" 메뉴를 통해 같은 방식으로 검증할 수 있습니다. 헤더 정보부터 본문 인코딩까지 모두 확인 가능합니다.
CSRF: 같은 메커니즘, 다른 의도
CSRF(Cross-Site Request Forgery) 는 로그인된 사용자의 권한을 도용해, 사용자가 의도하지 않은 요청을 공격자가 대신 실행하게 만드는 공격입니다.
전형적인 시나리오는 이렇습니다.
- 사용자가
bank.com에 로그인하고 로그아웃하지 않은 채로 다른 사이트를 탐색합니다. - 공격자가 만든 페이지
evil.com에 다음과 같은 코드가 숨어 있습니다.
<img src="https://bank.com/transfer?to=hacker&amount=1000000">
- 사용자가 그 페이지를 여는 순간, 브라우저는
bank.com으로 자동 요청을 보냅니다. 이때bank.com에 저장된 세션 쿠키도 함께 첨부됩니다. - 은행 서버 입장에서는 정상 로그인된 사용자의 요청과 구분할 수 없으므로 송금이 처리됩니다.
눈치채셨다면 트래킹 픽셀과 동일한 메커니즘입니다. 차이는 단지 요청이 추적용 픽셀로 가느냐, 송금 엔드포인트로 가느냐일 뿐입니다.
XSS와의 차이
CSRF는 비슷한 이름의 XSS와 종종 혼동됩니다. 핵심 차이를 짧게 정리하면 다음과 같습니다.
| 구분 | CSRF | XSS |
|---|---|---|
| 공격 본질 | 사용자 권한으로 요청 실행 | 사용자 브라우저에서 스크립트 실행 |
| 악용하는 신뢰 | 서버가 사용자를 신뢰함 | 사용자가 사이트를 신뢰함 |
| 데이터 탈취 | 직접적으로는 어려움 | 쿠키나 입력값 탈취 가능 |
CSRF는 "요청만" 가능합니다. 응답을 읽을 수 없기 때문에 데이터 자체를 빼내기는 어렵습니다. 대신 송금, 비밀번호 변경, 권한 변경 같은 상태 변경 작업을 시킬 수 있습니다.
방어 기법
표준적인 방어책은 몇 가지가 있습니다.
CSRF 토큰이 가장 전통적인 방식입니다. 서버가 폼을 렌더링할 때마다 예측 불가능한 토큰을 발급하고, 요청이 들어오면 그 토큰을 검증합니다. 외부 사이트는 이 토큰을 알 수 없으므로 공격이 차단됩니다.
// Spring Security 설정 예시
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
SameSite 쿠키 속성은 더 근본적인 해법입니다. 쿠키에 SameSite=Lax 또는 Strict 속성을 주면, 다른 사이트에서 발생한 요청에는 쿠키가 자동 첨부되지 않습니다. 2020년부터 Chrome이 기본값을 Lax로 바꿨기 때문에, 명시적으로 SameSite=None을 설정하지 않은 쿠키는 상당 부분 자동으로 보호됩니다.
이 외에 자주 쓰는 방어책은 다음과 같습니다.
- Origin/Referer 헤더 검증: 요청 출처가 자기 도메인인지 확인
- 상태 변경 요청은 절대 GET을 쓰지 않기: REST 원칙대로 POST/PUT/DELETE 사용
- 민감한 작업에는 재인증: 비밀번호 재입력이나 2FA
토큰 기반 인증과의 관계
JWT 같은 토큰을 Authorization 헤더로 보내는 구조에서는 CSRF가 원천 차단됩니다. 브라우저가 자동으로 첨부하는 건 쿠키이지 커스텀 헤더가 아니기 때문입니다. 다만 이 경우 토큰을 어디에 저장하느냐에 따라 XSS 위협이 더 커집니다. CSRF와 XSS는 일정 부분 트레이드오프 관계라는 점은 인증 구조를 설계할 때 기억할 만한 포인트입니다.
두 이야기가 가리키는 곳
트래킹 픽셀과 CSRF를 나란히 놓고 보면, 공통적으로 작동하는 원리는 결국 다음 세 가지입니다.
- 사용자의 단순한 행동(메일 열기, 페이지 방문)이 트리거가 됩니다.
- 클라이언트(브라우저 또는 메일 앱)가 자동으로 외부 요청을 보냅니다.
- 그 요청에는 사용자의 인증 정보나 식별 정보가 함께 실려 갑니다.
이 세 가지가 한 줄로 이어질 때, 누군가에게는 마케팅 지표가 되고 누군가에게는 공격 통로가 됩니다. 그래서 웹 보안 설계의 출발점은 "내 시스템에서 어떤 자동 요청이 발생하고, 거기에 무엇이 함께 전송되는가" 를 파악하는 데 있다고 생각합니다.
메일 원본을 직접 까보면 알 수 있듯이, 우리가 일상적으로 주고받는 데이터는 생각보다 많은 것을 담고 있습니다. 쿠키 정책 한 줄, 토큰 검증 하나가 시스템 무결성을 좌우합니다. 그리고 그 판단을 위해서는 추적과 공격이 사실 같은 원리 위에 서 있다는 점을 이해해두는 편이 좋습니다.
참고
- OWASP — Cross-Site Request Forgery Prevention Cheat Sheet
- RFC 2047 (MIME Part Three: Message Header Extensions for Non-ASCII Text)
- RFC 6265 (HTTP State Management Mechanism)
- RFC 3798 (Message Disposition Notification)
- MDN Web Docs — SameSite cookies