---
title: "X-Forwarded-For 에 /32가 붙어 IP allowlist가 깨질 때"
description: "허용 IP 목록에 분명히 있는데 계속 차단되던 원인은 req.ip 끝에 붙은 /32 였습니다. 감사 로그로 원인을 찾고 IP 를 정규화해 해결한 과정입니다."
date: 2026-11-09
updated: 2026-11-09T06:43:22.141Z
tags: [express, security, proxy, x-forwarded-for, debugging]
canonical: https://blog.wooncloud.com/posts/x-forwarded-for-cidr-suffix-ip-allowlist
---

관리 콘솔에 공인 IP allowlist 게이트를 걸었습니다. 허용 IP 목록(`["203.0.113.7"]`)에 든 IP에서만 들어올 수 있고, 아니면 404로 존재 자체를 숨기는 방식입니다. ALB와 CDN 뒤에 있는 Express 앱이고, `app.set("trust proxy", 1)`로 `req.ip`가 실제 클라이언트 IP를 가리키게 해 뒀습니다.

그런데 제 공인 IP가 분명히 목록에 있는데도 계속 차단됐습니다.

![CIDR 접미사가 남은 IP는 allowlist와 문자열이 어긋나 막히지만, 정규화하면 통과한다.](/images/posts/x-forwarded-for-cidr-suffix-ip-allowlist/2d80cb5a-7613-48c5-b5b9-8bb68855db9f.webp)

## 값은 맞는데 왜 막힐까

거부 로그에 찍힌 클라이언트 IP를 보니 이랬습니다.

```text
203.0.113.7/32
```

끝에 `/32`(CIDR 마스크)가 붙어 있었습니다. 게이트는 단순 문자열 비교였으니 당연히 안 맞습니다.

```ts
allowedIps.includes(clientIp) // ["203.0.113.7"].includes("203.0.113.7/32") → false
```

앞단 프록시 체인 어딘가에서 `X-Forwarded-For`에 클라이언트 IP를 `203.0.113.7/32` 형태로, 마스크까지 붙여 전달했습니다. Express `req.ip`도 그 값을 그대로 받아 `/32`가 남은 겁니다. IP는 맞는데 표기 한 글자 차이로 allowlist와 안 맞은 거죠.

`req.ip`가 항상 깨끗한 `a.b.c.d`일 거라고 가정하면 안 됩니다. 프록시 체인이 XFF에 무엇을 넣느냐에 따라 `::ffff:`(IPv4-mapped IPv6), 포트, CIDR 마스크 같은 게 섞여 들어올 수 있습니다.

![프록시가 전달한 IP에 /32 가 붙어 있으면 allowlist 문자열 비교가 실패해 404로 막히고, ::ffff: 와 /32 를 떼는 정규화를 거치면 통과한다.](/images/posts/x-forwarded-for-cidr-suffix-ip-allowlist/96270e3c-12ea-457f-a180-34c789b4c6fc.svg)

## 결정적 단서는 감사 로그

원인을 바로 찾은 건 감사 로그 덕분이었습니다. 어느 검사가, 어떤 실측 IP로 막았는지를 남겨 뒀거든요.

"거부됨"만 찍었다면 값이 맞는데 왜 막히는지 한참 헤맸을 겁니다. `reason=ip`와 서버가 실제로 본 IP 문자열을 같이 남겨 둔 덕에 `/32`를 바로 발견했습니다.

## 정규화로 풀기

IP 정규화 단계에서 CIDR 접미사를 제거해 호스트 표기로 통일했습니다. 비교 로직과 allowlist는 그대로 두고 들어오는 IP만 정규화합니다. 위조 방어에는 영향이 없습니다.

```ts
function normalizeClientIp(req: Request): string {
  let ip = req.ip || req.socket?.remoteAddress || "";
  if (ip.startsWith("::ffff:")) ip = ip.slice(7); // IPv4-mapped IPv6 → IPv4
  const slash = ip.indexOf("/");
  if (slash !== -1) ip = ip.slice(0, slash);       // CIDR 마스크(/32, /128) 제거
  return ip;
}
```

급하게 복구해야 한다면 코드 배포 없이 allowlist에 게이트가 실제로 보는 값(`203.0.113.7/32`)을 그대로 넣어 푸는 우회도 가능합니다. 다만 표기에 기대는 임시방편이라, 정석은 정규화입니다.

## 참고

- https://expressjs.com/en/guide/behind-proxies.html
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
