Java & Spring/Spring Security

07. 악용 보호

승주우에요 2026. 8. 6. 07:49
악용 보호 - Spring Security

악용 보호

CORS, CSRF, SameSite — 웹 보안의 세 가지 핵심 방어 메커니즘

웹 애플리케이션은 출처를 넘나드는 요청(Cross-Origin)과 위조 요청(CSRF)에 노출되어 있다. CORS는 브라우저가 허용된 출처의 교차 요청만 허용하도록 제어하고, CSRF는 서버가 서명된 토큰으로 정상 요청을 검증한다. SameSite 속성은 쿠키 전송 범위를 출처 수준에서 제한하여 CSRF를 추가로 방어한다.

1. CORS (Cross-Origin Resource Sharing)

동일 출처 정책 (Same-Origin Policy)

브라우저는 기본적으로 출처(Origin = 프로토콜 + 도메인 + 포트)가 다른 리소스 요청을 차단한다. 예를 들어 http://localhost:3000의 프론트엔드가 http://localhost:8080의 API를 호출하면 브라우저가 이를 교차 출처 요청으로 인식하여 차단한다.

CORS는 서버가 응답 헤더에 허용 정책을 명시함으로써 브라우저의 차단을 해제하는 표준 메커니즘이다.

Simple Request vs Preflight Request

구분 Simple Request Preflight Request
조건 GET/POST/HEAD + 단순 헤더 + 단순 Content-Type PUT/DELETE, 커스텀 헤더, JSON Content-Type 등
동작 바로 요청 전송 → 서버 응답 헤더 확인 OPTIONS 예비 요청 → 허용 확인 → 본 요청
브라우저 처리 응답에 Access-Control-Allow-Origin 없으면 차단 OPTIONS 응답이 거부되면 본 요청 차단
sequenceDiagram participant B as 브라우저 (localhost:3000) participant S as 서버 (localhost:8080) Note over B,S: Simple Request B->>S: GET /api/data\nOrigin: http://localhost:3000 S-->>B: 200 OK\nAccess-Control-Allow-Origin: http://localhost:3000 Note over B,S: Preflight Request B->>S: OPTIONS /api/data\nOrigin: http://localhost:3000\nAccess-Control-Request-Method: PUT\nAccess-Control-Request-Headers: Authorization S-->>B: 204 No Content\nAccess-Control-Allow-Origin: http://localhost:3000\nAccess-Control-Allow-Methods: PUT\nAccess-Control-Allow-Headers: Authorization B->>S: PUT /api/data (본 요청) S-->>B: 200 OK

Spring Security CORS 설정

Spring Security는 cors() DSL을 통해 CorsFilter를 필터 체인 앞단에 등록한다. CorsConfigurationSource로 허용 정책을 세밀하게 제어할 수 있다.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .cors(cors -> cors.configurationSource(corsConfigurationSource()))
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("http://localhost:3000"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-CSRF-TOKEN"));
    config.setAllowCredentials(true);  // 쿠키/인증 헤더 허용 시 true
    config.setMaxAge(3600L);           // Preflight 캐시 시간(초)

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}
주의: allowedOrigins("*")allowCredentials(true)는 동시에 사용할 수 없다. 자격증명을 허용할 때는 반드시 구체적인 출처를 지정해야 한다.

2. CSRF (Cross-Site Request Forgery)

공격 원리

사용자가 은행 사이트에 로그인한 상태에서 악성 사이트를 방문하면, 악성 사이트의 스크립트가 사용자 브라우저의 쿠키를 이용해 은행 API에 위조 요청을 보낼 수 있다. 브라우저는 쿠키를 자동으로 전송하므로 서버는 정상 요청과 구분하기 어렵다.

토큰 기반 방어

서버가 세션별 고유한 CSRF 토큰을 발급하고, 상태 변경 요청(POST/PUT/DELETE)에서 이 토큰이 포함되어 있는지 검증한다. 악성 사이트는 동일 출처 정책으로 인해 토큰 값을 읽을 수 없으므로 위조 요청에 토큰을 포함시킬 수 없다.

CSRF 활성화/비활성화

// Spring Security 기본값: CSRF 보호 활성화
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // 기본값은 csrf() 활성화
    http.csrf(Customizer.withDefaults());

    // REST API 서버처럼 STATELESS 세션을 사용하는 경우 비활성화
    http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

    return http.build();
}
CSRF 비활성화 기준: 세션 쿠키를 사용하지 않는 순수 REST API(JWT Bearer Token 방식)는 CSRF 위협이 없으므로 비활성화해도 안전하다. 반면 세션 기반 인증은 반드시 CSRF 보호를 유지해야 한다.

3. CSRF 토큰 관리

CsrfTokenRepository

CSRF 토큰의 저장 방식은 두 가지 구현체 중 선택한다.

구현체 저장 위치 기본 파라미터명 기본 헤더명
HttpSessionCsrfTokenRepository 서버 세션 _csrf X-CSRF-TOKEN
CookieCsrfTokenRepository 쿠키 (XSRF-TOKEN) _csrf X-XSRF-TOKEN
// HttpSession 방식 (기본값)
http.csrf(csrf -> csrf
    .csrfTokenRepository(new HttpSessionCsrfTokenRepository())
);

// Cookie 방식 (SPA용 — 프론트엔드가 쿠키 값을 읽어 헤더에 추가)
http.csrf(csrf -> csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);

CsrfTokenRequestHandler

요청에서 CSRF 토큰을 추출하는 방식을 담당하는 인터페이스다. 기본 구현은 XorCsrfTokenRequestAttributeHandler로, XOR 방식으로 토큰을 인코딩하여 BREACH 공격을 방어한다.

// XorCsrfTokenRequestAttributeHandler (기본값, BREACH 방어)
http.csrf(csrf -> csrf
    .csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
);

// SPA에서 커스텀 핸들러 사용
public final class SpaCsrfTokenRequestHandler extends XorCsrfTokenRequestAttributeHandler {
    private final CsrfTokenRequestHandler delegate = new CsrfTokenRequestAttributeHandler();

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       Supplier csrfToken) {
        // XOR 방식으로 처리 (BREACH 방어)
        super.handle(request, response, csrfToken);
    }

    @Override
    public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
        // 요청 헤더에 CSRF 토큰이 있으면 delegate로 처리 (SPA 방식)
        if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
            return super.resolveCsrfTokenValue(request, csrfToken);
        }
        // Form 파라미터에서 추출 (MPA 방식)
        return delegate.resolveCsrfTokenValue(request, csrfToken);
    }
}

CSRF 토큰 지연 로딩

Spring Security 6부터 CSRF 토큰은 실제로 필요한 시점에 로딩되는 지연 로딩(DeferredCsrfToken)을 사용한다. GET 요청처럼 CSRF 검증이 불필요한 경우 토큰 생성/조회 비용이 발생하지 않는다.

CsrfFilter 동작 흐름

sequenceDiagram participant C as 클라이언트 participant CF as CsrfFilter participant REPO as CsrfTokenRepository participant NEXT as 다음 필터 C->>CF: GET /login CF->>REPO: loadDeferredToken() → DeferredCsrfToken 생성 CF->>NEXT: chain.doFilter() (GET은 검증 생략) NEXT-->>C: 응답 (토큰 포함) C->>CF: POST /login (_csrf=token123) CF->>REPO: loadDeferredToken() → 실제 토큰 로드 CF->>CF: 요청 토큰 vs 저장 토큰 비교 alt 토큰 일치 CF->>NEXT: chain.doFilter() NEXT-->>C: 로그인 성공 else 토큰 불일치 CF-->>C: 403 Forbidden end

4. CSRF 통합 (Forms, SPA, MPA)

HTML Forms (전통적 MPA)

Thymeleaf 등 서버 사이드 템플릿에서는 hidden input으로 CSRF 토큰을 자동 삽입한다.

<!-- Thymeleaf: 자동 삽입 -->
<form th:action="@{/transfer}" method="post">
    <!-- Spring Security가 자동으로 추가 -->
    <input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
    <input type="text" name="amount"/>
    <button type="submit">이체</button>
</form>

SPA (Single Page Application)

React/Vue 같은 SPA는 서버에서 HTML을 렌더링하지 않으므로 쿠키 방식을 사용한다. 프론트엔드는 XSRF-TOKEN 쿠키를 읽어 X-XSRF-TOKEN 헤더에 담아 전송한다.

// 서버 측: Cookie 방식 설정 (httpOnly=false로 JS가 읽을 수 있도록)
http.csrf(csrf -> csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
);
// 클라이언트 측 (JavaScript)
async function post(url, data) {
    // 쿠키에서 XSRF-TOKEN 값 읽기
    const csrfToken = document.cookie
        .split('; ')
        .find(row => row.startsWith('XSRF-TOKEN='))
        ?.split('=')[1];

    return fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-XSRF-TOKEN': csrfToken  // 헤더에 토큰 추가
        },
        credentials: 'include',  // 쿠키 포함
        body: JSON.stringify(data)
    });
}

MPA (HTML meta 태그 방식)

<!-- HTML <head>에 메타 태그로 삽입 -->
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>

<script>
// Ajax 요청 시 헤더에 추가
const token = document.querySelector('meta[name="_csrf"]').content;
const header = document.querySelector('meta[name="_csrf_header"]').content;
// jQuery.ajaxSetup({ headers: { [header]: token } });
</script>

5. SameSite 쿠키 속성

개념

SameSite는 쿠키가 교차 사이트 요청에 포함될 수 있는지를 제어하는 쿠키 속성이다. CSRF 공격의 핵심이 "다른 사이트에서 보낸 요청에 쿠키가 자동으로 포함되는 것"이므로, SameSite 속성으로 이를 원천 차단할 수 있다.

동작 권장 사용처
Strict 동일 사이트 요청에서만 쿠키 전송. 외부 링크로 이동 시에도 전송 안 함. 은행, 결제 등 보안이 최우선인 서비스
Lax 외부 링크 클릭(GET)은 허용, POST 등 상태 변경 요청은 차단. 대부분의 웹 서비스 (브라우저 기본값)
None 모든 교차 사이트 요청에 쿠키 전송. 반드시 Secure 속성 필요. OAuth, 외부 임베드 위젯 등 명시적 교차 사이트 필요 시

Spring Session과 SameSite 설정

Spring Security 자체는 SameSite 속성을 직접 제어하지 않는다. Spring SessionDefaultCookieSerializer를 통해 세션 쿠키에 SameSite를 설정한다.

// Spring Session + DefaultCookieSerializer로 SameSite 설정
@Bean
public CookieSerializer cookieSerializer() {
    DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    serializer.setSameSite("Lax");    // "Strict", "Lax", "None" 중 선택
    serializer.setUseSecureCookie(true); // SameSite=None이면 반드시 true
    return serializer;
}
# application.yml (Spring Boot 내장 서버의 세션 쿠키 설정)
server:
  servlet:
    session:
      cookie:
        same-site: lax       # strict / lax / none
        secure: true         # SameSite=None은 Secure 필수
        http-only: true
CSRF vs SameSite: SameSite=Lax/Strict는 CSRF를 효과적으로 방어하지만, 오래된 브라우저 호환성과 서드파티 통합을 고려해야 한다. Spring Security의 CSRF 필터와 SameSite를 함께 사용하면 이중 방어가 된다.

정리

  • CORS: 브라우저의 동일 출처 정책을 허용 범위 내에서 완화. CorsConfigurationSource로 허용 출처/메서드/헤더를 명시적으로 설정.
  • CSRF Preflight: OPTIONS 요청으로 실제 요청 전 서버 허용 여부 확인.
  • CSRF 토큰: 세션별 고유 토큰을 상태 변경 요청마다 검증. HttpSession 방식(서버 저장)과 Cookie 방식(클라이언트 저장) 선택 가능.
  • XorCsrfTokenRequestAttributeHandler: XOR 인코딩으로 BREACH 공격을 방어.
  • SPA CSRF 통합: CookieCsrfTokenRepository.withHttpOnlyFalse()로 JS가 쿠키를 읽어 X-XSRF-TOKEN 헤더로 전송.
  • SameSite=Lax: 브라우저 기본값. 외부 POST 요청에서 쿠키를 차단하여 CSRF를 추가 방어.
  • REST API: JWT Bearer Token + STATELESS 세션이라면 csrf().disable() 안전.

'Java & Spring > Spring Security' 카테고리의 다른 글

08. 인증 프로세스 전체 흐름  (0) 2026.08.06
06. 예외 처리  (0) 2026.08.05
05. 세션 관리  (0) 2026.08.05
04. 인증 상태 영속성  (0) 2026.08.04
03. 인증 아키텍처  (0) 2026.08.04