🖥️ 실무 : Referer로 망분리를 시도했다가 배운 것들 (feat. Stateless vs Stateful)

공부하는 개발자

about 오늘도 뚝딱🔨 뚝딱 하는 주니어 개발자👩🏻‍💻

북마크

고정글

최근 글

🖥️ 실무 : Referer로 망분리를 시도했다가 배운 것들 (feat. Stateless vs Stateful)

category 🖥️ DevOpѕ
date
read
반응형

 

 

 

 

 

 


⟡ 인트로

OKTA로 인증 방식을 전환하면서 기존 SSO 인증서가 담당하던 내/외부망 구분 기능이 사라졌다.

처음에는 HTTP Referer 헤더를 이용해 내부망 사용자를 판별하려 했으나,

즐겨찾기 접속·OKTA 리다이렉트·forward 등 실제 운영 환경에서 Referer가 소실되는 경우가 너무 많아

정상 내부망 사용자까지 차단되는 문제가 발생했다.

결국 Stateless한 Referer를 버리고, 서버가 상태를 기억하는 세션 플래그(Session Flag) 방식으로 전환하여 문제를 해결했다. 이 글은 그 과정에서 Referer가 무엇이고 왜 접근 제어의 신뢰 기반이 될 수 없는지를 정리한 기록이다.

 

 

 

 

 

 


⟡ 개발환경

AS-IS TO-BE

SSO PKI 인증서 기반 내/외부 구분 OKTA SAML 인증 기반
인증서 유무로 VDI 여부 판단 세션 플래그(INTERNAL_VERIFIED)로 판단
UserInterceptor (비활성, 데드코드) AuthenticationInterceptor (활성)
Java 1.7 / Spring Framework 4.x Java 17 / Spring Boot 3.x

 

 

 

 

 


⟡ 문제 상황

 

 

 

기존 방식이 사라진 이유

레거시 코드의 인터셉터에는 SSO 인증서를 검증하는 로직이 있었다.

사용자 PC(또는 VDI)에 설치된 사내 인증서 모듈을 호출해서 VDI 여부를 판별했던 것이다.

 

VDI 사용자  → 인증서 모듈 실행 성공 → SSO 통과 ✅
외부 사용자 → PC에 사내 인증서 없음 → 모듈 로드 실패 → 차단 ❌

 

브라우저가 보내는 헤더(텍스트)를 믿은 게 아니라, PC 자체에 설치된 암호화된 인증서라는 물리적 증거를 검사했기 때문에

망 분리가 가능했다. 하지만 OKTA로 전환되면서 이 인증서 검증 로직이 사라졌고,

애플리케이션 입장에서는 "이 사람이 VDI 안에 있는가"를 판단할 방법이 없어진 상태가 됐다.

 

 

왜 Referer를 먼저 시도했는가

팀 내에서 최초로 나온 아이디어는 HTTP Referer 헤더를 이용하는 것이었다.

요구사항은 아래 두 조건 중 하나를 만족하면 내부 사용자로 보자는 것이었다.

 

  • Referer 헤더에 내부망 포털 도메인([내부망 포털])이 포함되어 있음
  • 특정 관문 경로(/main.do)로 직접 접근함

 

코드로 표현하면 아래와 같다.

 

private boolean isInternalAccess(String referer, String requestPath) {
    // 조건 A: Referer 체크
    if (referer != null && referer.contains("[내부망 포털]")) {
        return true;
    }
    // 조건 B: 관문 경로 체크
    if (requestPath != null && requestPath.startsWith("/main")) {
        return true;
    }
    return false;
}

 

 

겉보기에는 합리적인 것 같았다. 하지만 실제 배포 테스트를 해보니 두 가지 구조적 문제가 드러났다.

 

 

 

 

 


⟡ 1번째 시도: Referer 헤더 기반 내/외부 판단 (실패)

 

 

시도한 이유

내부망 포털에서 링크를 클릭해 들어오는 경우, 브라우저가 자동으로 출발지 URL을 Referer 헤더에 담아 전송한다.

이 헤더를 인터셉터에서 읽어서 내부망 도메인 포함 여부를 판별하는 방식이다.

 

 

문제점 1: 즐겨찾기·주소창 직접 입력 시 Referer는 null

VDI 사용자라도 크롬을 새로 켜고 주소창에 직접 URL을 입력하거나 즐겨찾기로 접속하면,

브라우저는 Referer 헤더를 아예 보내지 않는다. 서버에 도착하는 HTTP 요청을 보면 이렇다.

GET /main.do HTTP/1.1
Host: [내부서버]
User-Agent: Mozilla/5.0 ...
Cookie: JSESSIONID=...
(Referer 헤더 없음)

 

 

인터셉터 입장에서는 Referer가 null이므로 "출발지가 없다 → 외부망이다"라고 오판하게 된다.

정상 VDI 사용자가 차단되는 것이다.

 

 

문제점 2: forward 또는 OKTA 리다이렉트 시 Referer 증발

/main.do 조건을 통과해 세션 검증 없이 인터셉터를 통과했다고 하자.

이후 forward:/view/p/login.do로 내부 포워딩되는 순간, 서버 내부에서 새로운 요청이 생성되기 때문에

Referer가 비어버린다. OKTA 302 리다이렉트 이후에도 동일한 현상이 발생한다.

/main.do (통과) → forward → /view/p/login.do
→ 인터셉터 재진입
→ Referer: null
→ 차단 ❌

 

 

어느 레이어에서 검사해도 결과는 동일하다.

검사 위치 실행 코드 결과

Filter request.getHeader("Referer") null → 차단
Interceptor request.getHeader("Referer") null → 차단
Controller request.getHeader("Referer") null → 차단
JSP/JS document.referrer "" → 차단

 

 

 

핵심은 코드의 위치 문제가 아니라, 데이터의 부재 문제다.

브라우저가 제공하지 않는 정보는 서버의 어떤 레이어에서도 만들어낼 수 없다.

 

 

 

배운 점

Referer는 Stateless 방식이다. "지금 이 순간의 요청"이 어디서 왔는지만 알 뿐"
이 사람이 아까 어떤 경로로 들어왔는지"는 기억하지 못한다.
접근 제어처럼 연속적인 상태를 기억해야 하는 로직에는 태생적으로 부적합하다.

 

 

 

 

 

 


⟡ 최종 해결: 세션 플래그(Session Flag) 방식

 

 

 

 

Referer를 버리고 세션(Session)으로 관점을 바꿨다.

세션은 브라우저를 닫기 전까지 상태를 기억하는 Stateful한 저장소다.

아이디어는 단순하다.

 

 

/main.do 관문을 통과한 사람의 세션에 "내부망 인증 완료" 도장을 찍어두고,
이후 모든 요청에서는 Referer를 보지 않고 이 도장의 유무만 확인한다.

 

 

 

 

핵심 설계

1. 상수 정의 — 관문 URL 변경 시 이 한 곳만 수정하면 된다.

public final static class NetworkAccess {
    public static final String GATEWAY_PATH                  = "/main.do";
    public static final String OKTA_SAML_CALLBACK_PATH       = "/login/okta-saml-check.do";
    public static final String SESSION_KEY_INTERNAL_VERIFIED = "INTERNAL_VERIFIED";
}

 

 

 

2. 관문 컨트롤러 — /main.do 진입 시 세션에 플래그를 세팅한다.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;

@Controller
public class MainRedirectController {

    @RequestMapping(NetworkAccess.GATEWAY_PATH)
    public ModelAndView redirectMainToLogin(HttpServletRequest request) {
        // 내부망 진입 도장 찍기
        request.getSession().setAttribute(
            NetworkAccess.SESSION_KEY_INTERNAL_VERIFIED,
            Boolean.TRUE
        );
        return new ModelAndView("forward:/view/p/login.do");
    }
}

 

 

 

3. 인터셉터 판단 로직 — Referer 대신 세션 플래그만 본다.

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

private boolean isInternalAccess(HttpServletRequest request, String requestPath) {

    // 1. /main.do 관문 진입: 무조건 통과 (세팅은 컨트롤러에서)
    if (requestPath != null
            && requestPath.indexOf(NetworkAccess.GATEWAY_PATH) > -1) {
        return true;
    }

    // 2. OKTA SAML 콜백: 세션이 재생성되므로 이 시점에 플래그 재세팅
    if (requestPath != null
            && requestPath.indexOf(NetworkAccess.OKTA_SAML_CALLBACK_PATH) > -1) {
        request.getSession().setAttribute(
            NetworkAccess.SESSION_KEY_INTERNAL_VERIFIED,
            Boolean.TRUE
        );
        return true;
    }

    // 3. 그 외 모든 요청: 세션 플래그 유무만 확인
    HttpSession session = request.getSession(false);
    return session != null
        && session.getAttribute(NetworkAccess.SESSION_KEY_INTERNAL_VERIFIED) != null;
}

 

 

 

 

 

처리 흐름

VDI 사용자 → /main.do 접속
  → 세션에 INTERNAL_VERIFIED=true 세팅
  → OKTA 인증 완료 (세션 재생성 시 콜백에서 재세팅)
  → 이후 모든 요청: 세션 플래그 확인 → 통과 ✅

외부 사용자 → /view/p/login.do 직접 접속
  → 세션 플래그 없음
  → external-notice.jsp 차단 화면 ❌

 

 

 

 

 

왜 OKTA 콜백에서 플래그를 재세팅해야 하는가

OKTA SAML 인증이 완료되면 Session Fixation 공격 방지를 위해 세션 ID가 교체된다.

기존 세션에 저장해둔 INTERNAL_VERIFIED 플래그도 함께 사라지게 된다.

이를 방치하면 로그인 직후 AJAX 요청이 전부 차단된다.

/login/okta-saml-check.do 콜백 진입 시점에 플래그를 재세팅하면 이 문제를 방어할 수 있다.

 

 

 

 

 

 

 


 

 

 

⟡ 배운 점

  • Referer는 "지금"만 알고 세션은 "흐름 전체"를 기억한다.
    접근 제어처럼 요청 간의 맥락이 필요한 로직은 Stateless한 헤더가 아니라
    Stateful한 세션을 신뢰 기반으로 삼아야 한다.
  • 데이터의 부재는 코드의 위치로 해결할 수 없다.
    Referer 검사를 Filter·Interceptor·Controller·JSP 어디로 옮겨도 결과는 동일하다.
    "어디서 검사하는가"가 아니라 "검사할 값이 존재하는가"의 문제이기 때문이다.
  • 완벽한 망분리를 원한다면 클라이언트 IP 화이트리스트나 인프라 레벨 정책이 유일한 해답이다.
    브라우저가 선택적으로 전송하는 헤더(Referer)에 의존하는 방식은 언제든 우회될 수 있다.
    세션 플래그 방식은 현실적인 절충안이지, 완벽한 보안 솔루션이 아니다.
반응형