GET 로그아웃 차단과 동일 출처 POST 검증

이 글의 목차 8개 항목

문제와 원인

GET 로그아웃과 다른 출처·Origin 없는 POST에서도 세션·OAuth 대기 쿠키 2개를 만료시켰다. GET 라우트가 로그아웃 서비스를 호출했고, 서비스는 메서드와 출처를 검사하기 전에 쿠키 만료 응답을 만들었다.

수정 전 코드

route.ts

export const GET = (request: Request) =>
  withApiErrorBoundary(request, () => logoutGoogleAuth(request));

logoutGoogleAuth

수정 전 쿠키 만료와 기본 302 리다이렉트:

return redirectResponse(new URL(returnTo, origin).toString(), [
  clearGoogleSessionCookie(request.url),
  clearGooglePendingCookie(request.url),
]);

수정 후 코드

route.ts

export const POST = (request: Request) =>
  withApiErrorBoundary(request, () => logoutGoogleAuth(request), {
    code: "BACKEND_UNAVAILABLE",
    message: "서비스를 준비하지 못했습니다. 잠시 후 다시 시도해 주세요.",
  });

logoutGoogleAuth

라우트를 POST로 바꾸고 서비스에서 메서드→Origin→쿠키 만료 순서로 처리했다. POST 이외의 요청은 HTTP 405와 Allow: POST를 반환한다.

if (request.method !== "POST") {
  return new Response(null, {
    status: 405,
    headers: { Allow: "POST" },
  });
}

Origin이 요청 URL의 origin과 다르거나 누락되면 HTTP 403으로 종료한다.

const requestOrigin = new URL(request.url).origin;
if (request.headers.get("origin") !== requestOrigin) {
  return Response.json(
    { error: "허용되지 않은 요청 출처입니다." },
    { status: 403, headers: noStoreHeaders() },
  );
}

두 검사를 통과한 요청에 쿠키 만료 지시 2개와 HTTP 303을 반환한다. 복귀 주소는 기존 safeRelativeReturnPath()로 검증하고, 후속 페이지는 GET으로 읽는다.

return redirectResponse(new URL(returnTo, origin).toString(), [
  clearGoogleSessionCookie(request.url),
  clearGooglePendingCookie(request.url),
], 303);

검증 결과

Node.js에서 원본 전후 logoutGoogleAuth()에 같은 합성 요청 4개를 넣고 HTTP 상태와 Set-Cookie를 비교했다. Wrangler의 로컬 Worker에도 같은 메서드·Origin 조건으로 HTTP 요청을 보냈다.

입력 조건수정 전 HTTP / 만료 쿠키수정 후 HTTP / 만료 쿠키
GET, Origin 없음302 / 2개405 / 0개
POST, 다른 Origin302 / 2개403 / 0개
POST, Origin 없음302 / 2개403 / 0개
POST, 동일 Origin302 / 2개303 / 2개

거절된 세 조건의 Set-Cookie는 비어 있고, GET 거절 응답의 Allow는 POST다. 정상 POST의 복귀 주소는 전후 같은 내부 학습 경로를 유지했다.

메서드·Origin별 HTTP 상태와 만료 쿠키 수원본 보기 (새 창)

메서드·Origin별 HTTP 상태와 만료 쿠키 수

100%원본 보기 (새 창)

확대한 이미지는 상하좌우로 이동할 수 있습니다. 키보드는 이미지 영역에 초점을 두고 방향키를 사용하세요. Esc로 닫습니다.