← 실험실 웹 인프라 해설 · 보안

X-Forwarded-For: IP를 어디까지 믿어도 되는가

프록시 뒤의 서버는 방문자의 IP를 바로 볼 수 없습니다. 그래서 중간 서버들이 X-Forwarded-For라는 헤더에 앞에서 본 주소를 이어 적습니다. 문제는 이 값이 증명서가 아니라 요청 안에 들어온 문자열이라는 점입니다. 누가 썼고, 어느 구간까지 검증됐는지 모르면 로그인 제한·레이트 리밋·지역 판정이 전부 흔들립니다.

주제: HTTP headers · reverse proxy 정리일: 2026-08-16 적용 관점: 정적 사이트 + 이후 API

1. 한 장 요약

헤더는 기록이다프록시가 앞 단계에서 본 주소를 전달하는 관례입니다. 누가 어떤 규칙으로 썼는지가 핵심입니다.
헤더는 외부 입력이다클라이언트가 처음부터 값을 넣어 보낼 수 있습니다. 백엔드가 바로 믿으면 IP 스푸핑이 됩니다.
신뢰 경계가 먼저다신뢰할 프록시를 정하고, 그 프록시를 거쳐온 요청만 오른쪽에서 왼쪽으로 해석해야 합니다.

결론부터 말하면 "X-Forwarded-For의 첫 번째 IP가 진짜 방문자다"라고 읽으면 안 됩니다. 실제 소켓의 상대가 내가 아는 프록시인지 확인하고, 그 프록시들이 추가한 구간만 신뢰한 뒤, 오른쪽에서 왼쪽으로 보며 처음 만나는 검증되지 않은 유효 IP를 애플리케이션의 클라이언트 IP로 취급하는 식입니다.

IP를 알아내는 문제처럼 보이지만, 실제 문제는 어디까지가 우리 인프라가 보증한 데이터인지 그리는 일입니다.

2. 요청은 왜 IP를 여러 개 갖나

브라우저에서 애플리케이션까지 요청이 한 번에 가지 않는 경우가 많습니다. CDN, 로드 밸런서, 리버스 프록시를 차례로 지나면 마지막 백엔드가 직접 보는 상대 주소는 바로 앞 프록시가 됩니다. 앞에서 어떤 클라이언트가 연결했는지 알려주기 위해 프록시가 헤더를 이어 붙입니다.

방문자client

요청에 임의의 X-Forwarded-For 값을 넣을 수도 있습니다. 이 구간의 값은 아직 검증되지 않았습니다.

CDNedge proxy

자기가 본 연결 주소를 추가하거나, 설정에 따라 외부에서 온 헤더를 덮어씁니다.

로드 밸런서load balancer

다시 앞 단계의 정보를 전달합니다. 백엔드는 이 연결의 실제 상대 주소로 로드 밸런서도 볼 수 있습니다.

애플리케이션backend

헤더와 소켓 상대 주소를 함께 받습니다. 이때부터 어떤 값이 보증되는지 판단해야 합니다.

X-Forwarded-For: 1.2.3.4, 172.16.1.101, 28.178.124.142, 198.40.10.101

오른쪽 값이 항상 마지막 프록시의 주소인 것은 아닙니다. 마지막 프록시는 이미 TCP 연결의 상대 주소로 보이므로 헤더에는 보통 자기 주소를 다시 넣지 않습니다. 따라서 헤더만 보지 말고 실제 연결 상대 주소와 프록시 구성의 순서를 같이 봐야 합니다.

3. 어느 IP까지 믿을 것인가

신뢰 방식은 크게 두 가지입니다. 특정 프록시의 IP 대역을 관리하거나, 내 요청 경로에 프록시가 몇 겹인지 고정합니다. 어느 쪽이든 먼저 전제해야 할 것은 하나입니다. 백엔드가 인터넷에 직접 노출되어 있으면 헤더 신뢰 규칙이 무너집니다.

방식판정법대가
Trusted proxy list오른쪽에서 시작해 내가 관리하는 프록시 주소를 건너뛰고, 처음 만나는 유효한 외부 IP를 사용합니다.클라우드 프록시·오토스케일링 IP 대역을 계속 관리해야 합니다.
Trusted proxy count프록시가 N개라는 계약을 두고 오른쪽에서 N-1개를 건너뛴 위치를 사용합니다.요청 경로가 바뀌거나 우회 경로가 생기면 설정이 바로 낡습니다.

예를 들어 내 인프라가 로드 밸런서와 CDN 두 겹이고, 백엔드는 로드 밸런서에서만 접근할 수 있다고 합시다. 실제 상대 주소가 로드 밸런서인지 확인한 다음, 오른쪽에서 CDN 주소를 건너뛰고 그 왼쪽의 28.178.124.142를 클라이언트 후보로 삼습니다. 가장 왼쪽의 1.2.3.4는 방문자 쪽 프록시가 넣었을 수도 있으므로 보안 판정에는 쓰지 않습니다.

로깅에는 정규화한 전체 체인을 남길 수 있지만, 인증·접근제어·레이트 리밋에는 신뢰 경계 안에서 고른 하나의 값만 사용합니다.

4. 헤더를 파싱할 때 생기는 구멍

신뢰할 프록시를 정해도 파싱을 허술하게 하면 문제가 남습니다. 헤더는 사용자가 조작할 수 있는 입력이므로 값의 개수·길이·형식을 서버 코드가 감당할 수 있어야 합니다.

  • 유효하지 않은 IP를 거부합니다. 쉼표로 나눈 값이 모두 IPv4라고 가정하지 말고 IPv6, 포트가 붙은 표기, 공백을 처리합니다.
  • 중복 헤더를 정의된 방식으로 처리합니다. 여러 개의 같은 헤더를 합칠지, 애초에 거부할지 프레임워크와 프록시의 동작을 테스트합니다.
  • 길이와 개수를 제한합니다. 문자열을 그대로 DB나 로그에 쌓으면 로그 오염과 자원 고갈의 입력면이 됩니다.
  • 원문을 로그 포맷으로 실행하지 않습니다. IP처럼 보이는 문자열도 로그 시스템에 들어가기 전 정규화·이스케이프해야 합니다.
  • 프레임워크의 proxy trust 설정을 먼저 읽습니다. Express, Django, Rails 등은 신뢰 프록시 수나 대역을 설정하면 이 로직을 대신 처리하지만, 기본값을 확인하지 않고 켜면 안 됩니다.
의사 코드: 경계 밖의 값은 보안 판정에서 제외
peer = request.socket.remoteAddress
chain = parseAndValidate(request.headers["x-forwarded-for"])

if (!trustedProxy(peer)) {
  clientIp = peer
} else {
  clientIp = firstUntrustedFromRight(chain, TRUSTED_PROXY_RANGES)
}

이 코드는 설명용 모델이지 복사해서 운영에 넣는 완성 라이브러리가 아닙니다. 실제 구현에서는 IPv4-mapped IPv6, 프록시의 헤더 병합 방식, 플랫폼이 제공하는 표준 클라이언트 IP 헤더까지 포함해 테스트해야 합니다.

5. 우리 사이트에 적용하면

현재 cmore.dev는 index.html, style.css, 각 실험 페이지로 구성된 정적 사이트입니다. 이 구조에서는 방문자 IP를 읽어 인증하거나 레이트 리밋을 걸 백엔드가 없으므로, 브라우저 코드에 X-Forwarded-For를 추가하는 것은 할 일이 아닙니다.

지금static site

헤더를 기능처럼 붙이지 않습니다. 이 페이지처럼 개념과 위험을 기록하고, 정적 배포의 보안 헤더는 별도 설정으로 관리합니다.

API를 붙일 때future backend

진입 프록시를 하나 정하고, 백엔드를 직접 열지 않으며, 외부에서 온 XFF를 경계에서 덮어쓰거나 명시적으로 정리합니다.

판정할 때security use

인증·관리자 접근·요금·레이트 리밋에 헤더 원문을 쓰지 않고, 프레임워크의 신뢰 프록시 설정을 거친 정규화 값을 사용합니다.

특히 클라이언트에서 헤더를 읽어 신뢰하는 방식은 성립하지 않습니다. 브라우저가 보는 값은 서버의 신뢰 경계를 만들지 못하고, CORS나 프런트엔드 코드만으로 방문자 IP의 진위를 보증할 수도 없습니다. 이 문제는 프록시와 백엔드가 함께 결정해야 합니다.

6. 배포 전 체크리스트

  • 백엔드 직접 접근 차단: 공개 주소로 백엔드에 바로 요청해도 같은 인증·레이트 리밋 경로를 우회할 수 없는가?
  • 신뢰 프록시 명시: 프록시 IP 대역 또는 프록시 개수가 코드·배포 설정에 적혀 있고 변경 시 같이 검토되는가?
  • 경계에서 정리: 첫 외부 진입점이 외부에서 들어온 XFF를 그대로 전달하지 않고, 플랫폼이 보증한 값을 기준으로 다시 만드는가?
  • 실제 상대 주소 확인: 백엔드가 헤더만 보고 프록시를 믿지 않고 소켓 상대 주소도 검증하는가?
  • 파싱 테스트: 위조 값, 빈 값, 중복 헤더, 잘못된 IPv6, 긴 문자열, 쉼표·공백 변형을 테스트했는가?
  • 사용처 분리: 분석용 전체 체인과 보안 판정용 단일 IP를 같은 변수로 다루지 않는가?
경계 프록시에서 시작하는 최소 원칙
1. backend는 trusted proxy를 통해서만 접근 가능하게 한다.
2. trusted proxy는 외부 입력을 검증하고 전달 헤더를 정규화한다.
3. backend는 socket peer와 trusted proxy 설정을 함께 확인한다.
4. security decision에는 정규화된 client IP 하나만 사용한다.
5. 원문 체인은 길이·개수·형식을 제한한 뒤 관측용으로만 저장한다.
이 글은 HTTP Toolkit의 X-Forwarded-For 해설을 읽고, cmore.dev의 정적 사이트와 이후 API 설계에 필요한 판단만 독립적으로 다시 정리한 기록입니다. 원문의 예제와 표현을 복사한 글이 아니며, 운영 환경에서는 사용 중인 CDN·리버스 프록시·웹 프레임워크의 공식 문서를 기준으로 검증하세요.