악용 보호
CORS, CSRF, SameSite — 웹 보안의 세 가지 핵심 방어 메커니즘
목차
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 응답이 거부되면 본 요청 차단 |
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();
}
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 동작 흐름
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 Session의 DefaultCookieSerializer를 통해 세션 쿠키에 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
정리
- 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 |