API
비밀번호 변경 API에 CSRF 확인을 추가하며 구분한 두 종류의 인증
쿠키 기반 로그인에서 비밀번호 변경 요청을 보호할 때, 인증과 CSRF 방어를 구분해 적용한 체크리스트.
로그인 확인만으로는 부족했던 요청
비밀번호 변경 API를 만들며 처음에는 세션 쿠키가 있으니 로그인 사용자만 호출할 수 있다고 생각했다. 그러나 쿠키 인증은 브라우저가 다른 사이트에서 보낸 요청에도 조건에 따라 쿠키를 실어 보낼 수 있다. 사용자가 내 서비스에 로그인한 채 악성 페이지를 열었을 때의 요청 위조가 CSRF다.
헷갈렸던 부분은 인증(authentication)과 요청 출처 검증을 같은 것으로 본 점이다. 세션은 “누구인가”를 확인하고, CSRF 방어는 “이 요청을 사용자가 의도한 화면에서 보냈는가”를 확인한다.
상태를 바꾸는 요청을 먼저 추렸다
조회 API에는 적용하지 않고, 이메일 변경·비밀번호 변경·결제 시작처럼 상태를 바꾸는 POST, PATCH, DELETE 요청부터 목록으로 만들었다. Next.js Route Handler에서는 Origin을 서버의 origin과 비교하고, 민감한 요청에는 별도 CSRF 토큰도 검증하도록 했다.
export function requireSameOrigin(request: Request) {
const origin = request.headers.get('origin');
const expected = process.env.APP_ORIGIN;
if (!origin || origin !== expected) {
return Response.json({ message: 'Invalid origin' }, { status: 403 });
}
}
Origin 검사만으로 모든 환경을 덮을 수는 없다. 서버 간 호출, 프록시, 개발 주소는 명시적으로 처리해야 한다. 그래서 운영 origin을 환경 변수로 두고, 와일드카드 비교를 허용하지 않았다.
쿠키 속성도 함께 점검했다
세션 쿠키에는 HTTPS 환경에서 Secure, JavaScript 접근을 줄이는 HttpOnly, 상황에 맞는 SameSite를 설정했다. SameSite=Lax가 도움이 되지만 그것만으로 모든 변경 요청의 보호를 끝냈다고 보지 않았다.
response.cookies.set('session', sessionId, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
});
비밀번호 변경에는 현재 비밀번호 재입력과 속도 제한도 추가했다. CSRF를 막아도 탈취된 세션에서의 변경까지 해결하는 것은 아니기 때문이다.
배포 전 체크리스트
- 변경 API가 쿠키 인증을 사용하는지 확인한다.
- 서버가
Origin또는Referer를 기대값과 비교하는지 본다. - 허용 origin을
*로 두지 않는다. - 세션 쿠키의
Secure,HttpOnly,SameSite를 실제 응답 헤더에서 확인한다. - 변경 성공·실패를 비밀번호 값 없이 감사 로그에 남긴다.
보안 코드는 큰 암호화 라이브러리보다, 요청 하나가 어디서 왔고 무엇을 바꾸는지 분리해서 보는 데서 시작했다.