누구나 인증 헤더를 붙여 보낼 수 있었다 — BFF 프록시에서 헤더를 지우고 다시 심은 이야기
BFF 프록시가 클라이언트 요청을 내부 API로 그대로 전달하면, 클라이언트가 실어 보낸 X-API-KEY·X-Auth 헤더도 함께 전달된다. 그 헤더를 신뢰하는 대신, 프록시 단에서 전부 삭제하고 서버가 가진 진짜 값으로 재주입해 위조 자체를 불가능하게 만든 과정.
관련 프로젝트: 멀티테넌트 예약 SaaS
내부 API 키를 클라이언트에 노출하지 않으려고 BFF 프록시를 세웠다. 그런데 프록시를 "요청을 그대로 넘겨주는 통로"로만 만들면, 막으려던 문제가 다른 자리에서 다시 열린다.
문제 — 프록시는 헤더까지 그대로 넘긴다
Next.js Route Handler로 만든 프록시(/proxy/[...path])는 들어온 요청을 내부 API로 포워딩한다. 목적은 단순했다 — API 키를 클라이언트 번들에 심지 않고 서버에만 두는 것. 여기까진 맞는 방향이다.
그런데 "요청을 그대로 넘긴다"를 문자 그대로 구현하면, 요청 헤더도 그대로 넘어간다. 클라이언트가 fetch에 X-API-KEY나 X-Auth-* 같은 헤더를 직접 실어 보내면, 프록시는 그걸 검증 없이 내부 API에 그대로 전달한다.
즉 API 키를 클라이언트 코드에서 지웠다고 해서 문제가 끝난 게 아니었다. 클라이언트가 헤더를 마음대로 채워 보낼 수 있는 자리가 여전히 열려 있었다. 브라우저 개발자도구에서 요청을 가로채 임의의 값을 넣은 헤더로 재전송하면, 프록시는 그걸 "정상 요청"으로 착각하고 내부 API에 넘긴다.
접근 — 클라이언트 헤더를 신뢰하지 않는다
고칠 지점은 하나였다. 인증 관련 헤더는 클라이언트가 보낸 값을 절대 쓰지 않는다.
// 개념 스케치 — 프록시 핸들러의 헤더 처리 흐름 (실제 구현 아님)
function buildUpstreamHeaders(clientHeaders) {
const headers = new Headers(clientHeaders);
// 클라이언트가 무엇을 보냈든 일단 전부 지운다
headers.delete("X-API-KEY");
for (const key of headers.keys()) {
if (key.toLowerCase().startsWith("x-auth-")) headers.delete(key);
}
// 서버가 들고 있는 진짜 값으로 다시 채운다
headers.set("X-API-KEY", process.env.INTERNAL_API_KEY);
headers.set("X-Auth-Token", getServerAuthToken());
return headers;
}순서가 중요하다. 먼저 지우고, 그다음 서버 값으로 채운다. "클라이언트 값이 있으면 두고, 없으면 채운다" 같은 조건부 로직은 쓰지 않았다 — 그러면 클라이언트가 값을 채워 보내는 순간 그 값이 그대로 살아남는다. 무조건 삭제 후 무조건 재주입이라야, 클라이언트가 무엇을 보내든 결과가 항상 같다.
왜 "검증"이 아니라 "삭제 후 재주입"인가
처음엔 "클라이언트가 보낸 헤더 값이 서버가 아는 값과 같은지 검증하면 되지 않나" 하는 생각도 했다. 하지만 이건 잘못된 방향이다.
- 검증하려면 클라이언트가 애초에 그 값을 알고 있어야 비교가 성립한다. API 키를 클라이언트에 알려주지 않는 게 목적이었으므로, 클라이언트가 "맞는 값"을 보낼 방법 자체가 없어야 정상이다.
- 검증 로직은 분기가 늘어난다. "있으면 검증, 없으면 통과" 같은 예외 경로가 생기는 순간, 그 경로가 곧 우회 지점이 된다.
- 반면 삭제 후 재주입은 클라이언트 입력을 아예 신뢰 경계 밖에 둔다. 클라이언트가 뭘 보내든 무의미해지므로, 검증이 필요 없어진다.
즉 "위조를 막는다"가 아니라 "위조가 성립할 자리 자체를 없앤다" 쪽으로 접근을 바꾼 것이다.
정리
| 구분 | before(단순 포워딩) | after(삭제 후 재주입) |
|---|---|---|
| 클라이언트가 인증 헤더를 채워 보내면 | 검증 없이 내부 API에 그대로 전달 | 프록시 단에서 삭제, 서버 값으로 교체 |
| API 키 노출 지점 | 클라이언트 번들엔 없지만, 헤더 조작 경로가 열려 있음 | 클라이언트는 애초에 값을 알 방법이 없음 |
| 신뢰 모델 | "클라이언트 요청을 그대로 믿는다" | "클라이언트 입력은 인증 관련 필드에서 전부 무시한다" |
API 키를 클라이언트 코드에서 지우는 것과, 클라이언트가 그 자리를 흉내 낼 수 없게 만드는 것은 다른 문제다. 전자만 하고 멈추면 프록시가 "검증 없는 통로"로 남는다. 프록시를 둔 이유가 신뢰 경계를 서버로 옮기는 것이었다면, 그 경계 안에서 클라이언트 입력을 다시 신뢰해선 안 된다.