WeeklyPaper: Spring 기반 웹 애플리케이션에서 발생할 수 있는 4가지 주요 보안 공격 (CSRF, XSS, 세션 고정, JWT 탈취)에 대해 설명하고, 각각에 대한 Spring Security 또는 일반적인 대응 전략을 설명하세요.
CSRF (Cross-Site Request Forgery)
CSRF(Cross-Site Request Forgery)는 사용자가 자신도 모르게 공격자가 의도한 요청을 특정 웹 애플리케이션에 보내도록 속이는 공격이다.
- 피해자가 로그인하여 인증 쿠키를 보유 중일 때, 공격자가 조작된 링크나 폼을 열게 하면 브라우저가 자동으로 쿠키를 첨부하여 서버에 요청을 보낸다. (쿠키는 브라우저에 저장이 된다)
- 예시 시나리오:
- 사용자가 은행 사이트에 로그인해서 세션 쿠키를 들고 있다.
- 공격자가 이메일로 악성 링크(<img src="https://bank.com/transfer?to=attacker&amount=1000">)를 삽입한다.
- 사용자가 메일을 열면 브라우저가 은행 서버에 요청을 보내고, 자동으로 세션 쿠키가 첨부된다.
- 서버는 정상적인 사용자 요청으로 인식하고 이체를 수행한다.
- 결과는 사용자의 의도와 다르게 권한 있는 요청이 실행된다.

대응 전략 1 - 동기화 토큰 (Synchronizer Token)
- 격을 방어하기 위한 가장 표준적이고 강력한 방법 중 하나다. 핵심 원리는 사용자가 보내는 요청이 정말로 사용자의 의도에 의해 시작된 것인지를 서버가 확인할 수 있는 비밀 값을 사용하는 것이다.
- 프로세스 예시
- 사용자가 웹사이트에 로그인하여 세션이 생성되면, 서버는 해당 세션을 위해 아무나 예측할 수 없는 무작위의 고유한 토큰(CSRF 토큰)을 생성한다. 이 토큰은 서버 측 세션에 저장된다.
- 서버는 사용자에게 비밀번호 변경, 글쓰기, 송금 등 민감한 작업을 수행하는 폼(Form) 페이지를 보낼 때, 이 CSRF 토큰을 <input type="hidden"> 태그를 이용해 폼 안에 몰래 숨겨서 전달한다.
-
<input type="hidden" name="csrf_token" value="a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6">
-
- 사용자가 폼을 작성하고 '송금' 버튼을 누르면, 브라우저는 폼 데이터와 함께 숨겨져 있던 CSRF 토큰 값도 서버로 함께 전송한다.
- 서버 측 검증: 요청을 받은 서버는 두 가지를 비교한다.
- 사용자 세션에 저장해 둔 CSRF 토큰
- 폼 데이터와 함께 전송된 CSRF 토큰
- 만약 두 값이 일치하면, 서버는 이 요청이 정상적인 경로를 통해 온 것이라고 신뢰하고 처리한다. 만약 두 값이 일치하지 않거나 폼에서 토큰이 전송되지 않았다면, 서버는 이 요청을 CSRF 공격으로 간주하고 거부한다.
- 이 방법이 효과적인 이유는 공격자는 서버가 사용자의 브라우저에만 몰래 심어놓은 CSRF 토큰의 값을 알 수 없다.

Spring Security의 CSRF 기본 보호 메커니즘
- Spring Security는 기본적으로 상태 유지 세션 기반 애플리케이션에서 CSRF 보호를 활성화한다. 서버는 요청 시 CSRF 토큰을 검증하여, 클라이언트가 의도적으로 요청을 보냈는지 확인한다.
- 앞서 설명된 동기화 토큰 패턴(Synchronizer Token Pattern)의 구현체다.
- 구성 요소:
- CsrfFilter: 모든 요청에 대해 CSRF 토큰을 검증하는 필터다.
- 사용자가 보낸 요청에 _csrf 파라미터나 헤더 값이 없는 경우 AccessDeniedException을 발생시킨다.
- 요청에 _csrf 파라미터나 헤더 값이 있으면 그 값을 추출하고, 저장소 (HttpSession / Cookie)에 보관된 토큰과 비교한다. 불일치하면 AccessDeniedException을 발생시킨다.
- CsrfToken: 서버가 세션별로 생성하여 클라이언트에 전달하는 토큰이다. 이 토큰은 매 요청마다 클라이언트가 전달해야 하며, 서버는 저장된 토큰과 비교하여 일치 여부를 검사한다
- Spring Security는 사용자가 최초로 세션을 생성하거나 페이지를 요청할 때 고유한 난수 기반 토큰을 생성한다. 이 토큰은 HttpSessionCsrfTokenRepository(기본 구현체) 또는 CookieCsrfTokenRepository에 저장된다.
- 난수는 SecureRandom 기반으로 생성이 되어서 예측이 불가능하고 위조가 어렵다.
- CsrfFilter: 모든 요청에 대해 CSRF 토큰을 검증하는 필터다.
토큰 생성 과정 예시
@Bean
public CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repo = new HttpSessionCsrfTokenRepository();
repo.setParameterName("_csrf");
repo.setHeaderName("X-CSRF-TOKEN");
return repo;
}
HTML 폼 예시 (Thymeleaf):
<form action="/transfer" method="post">
<input type="hidden" name="_csrf" value="${_csrf.token}" />
<button type="submit">송금</button>
</form>
Spring Security 6.x에는 CSRF가 기본적으로 활성화되어있다:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.enable())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
}
대응 전략 2 - 더블 서브밋 (Double Submit Cookie)
-
- 서버가 토큰 값을 세션에 저장하지 않는다는 점에서 동기화 토큰 패턴과 결정적인 차이가 있다. 서버는 상태를 유지할 필요 없이(stateless) 검증만 수행한다.
- 구현체는 CookieCsrfTokenRepository다. 기본적으로 사용되는 HttpSessionCsrfTokenRepository(동기화 토큰 패턴) 대신 이 구현체를 사용하도록 설정하면, Spring Security가 더블 서브밋 쿠키 방식으로 CSRF 보호를 처리한다.
- 작동 방법
- 서버는 사용자의 첫 요청 시, 예측 불가능한 토큰을 생성한다.
- 이 토큰을 세션에 저장하는 대신, 두 곳으로 나누어 클라이언트에게 전송한다.
- 응답 쿠키(Cookie)에 담아 보낸다.
- HTML 본문의 hidden 필드나 AJAX 요청을 위한 헤더 값으로도 보낸다.
- 이 토큰을 세션에 저장하는 대신, 두 곳으로 나누어 클라이언트에게 전송한다.
- 클라이언트의 요청
- 사용자가 데이터를 전송할 때, 브라우저는 쿠키에 담긴 토큰을 자동으로 요청에 포함시킨다.
- 동시에, 폼 데이터나 AJAX 요청 헤더에 있던 두 번째 토큰도 함께 서버로 전송된다.
- 서버의 검증
- 서버는 요청에 담겨 온 두 개의 토큰(쿠키의 토큰, 헤더/본문의 토큰)을 받는다.
- 서버는 세션을 확인할 필요 없이, 단순히 이 두 개의 값이 일치하는지만 비교한다.
- 두 값이 일치하면 신뢰할 수 있는 요청으로 판단하고, 일치하지 않으면 CSRF 공격으로 간주하여 차단한다.
- 서버는 사용자의 첫 요청 시, 예측 불가능한 토큰을 생성한다.
- 구현: 서버는 값 비교만, 저장 불필요 (서명 권장)

대응 전략 3 - SameSite 쿠키 값 설정
- SameSite는 CSRF 공격의 핵심 원리인 "다른 사이트에서 요청을 보낼 때 브라우저가 인증 쿠키를 자동으로 첨부하는 것" 자체를 막는 방식이다. 즉, 서버가 아닌 브라우저에게 직접 보안 규칙을 지시하는 것이다.
- SameSite는 3가지 정책 Strict, Lax, None이 있다. 브라우저는 이 정책을 보고 쿠키를 언제 전송할지 결정한다.
- SameSite=Lax (대부분의 브라우저 기본값): mybank.com과 전혀 관련 없는 악성 사이트(evil.com)에서 보내는 요청(POST, <img> 등)에는 세션 쿠키를 자동으로 첨부하지 않는다. 이 정책 하나만으로 대부분의 CSRF 공격이 원천 차단된다.
- SameSite=Strict: 가장 강력한 정책으로, 외부 사이트에서 링크를 클릭해 들어오는 경우에도 쿠키를 보내지 않는다.
- SameSite는 3가지 정책 Strict, Lax, None이 있다. 브라우저는 이 정책을 보고 쿠키를 언제 전송할지 결정한다.
- 공격자가 악성 사이트에서 사용자의 은행 사이트로 위조 요청을 보내도, 브라우저가 SameSite 정책에 따라 쿠키를 보내주지 않는다. 결국 서버는 인증되지 않은 요청으로 판단하고 이를 거부하게 된다.
- Spring Security
- Spring Security는 사용자의 세션을 관리하는 세션 쿠키 (일반적으로 JSESSIONID)를 생성한다.
- 개발자는 이 세션 쿠키가 생성될 때 SameSite=Lax/Strict attribute를 포함하도록 쿠키를 설정하고 브라우저로 보낸 수 있다. 이 설정이 적용되면, 해당 애플리케이션의 모든 인증 쿠키는 브라우저의 SameSite 정책에 따라 보호된다.
XSS (Cross-Site Scripting)
XSS는 공격자가 악의적인 스크립트를 웹 페이지에 삽입하여 사용자의 브라우저에서 실행되도록 하는 공격 기법이다.
- 주로 쿠키 탈취, 세션 하이재킹, 악성 사이트 리다이렉션, 피싱 등에 활용된다.

CSP(Content-Security-Policy)와 X-Content-Type-Options: nosniff는 현재 Spring Security의 XSS 방어에서 가장 많이 사용하는 방식이다.
Spring의 대응 전략 1 - X-Content-Type-Options: nosniff
- 문제 상황
- 서버가 Content-Type: text/plain으로 응답을 보냈는데, 내용물이 <script>alert('hacked');</script> 와 같은 자바스크립트 코드일 수 있다.
- 일부 브라우저는 "이건 그냥 텍스트가 아니라 스크립트 같은데?"라고 스스로 판단하여 멋대로 실행해 버릴 수 있다. 이를 MIME 스니핑(MIME Sniffing)이라고 부른다.
- 공격자는 이 점을 악용해 텍스트 파일인 척 위장한 악성 스크립트를 실행시킬 수 있다.
- 해결책
- 컨트롤러에서 정확한 Content-Type 지정
- 전역으로 X-Content-Type-Options: nosniff 헤더를 설정하면, 브라우저는 서버가 명시한 Content-Type을 절대적으로 신뢰하고 다른 해석을 하지 않는다.
- 서버가 'text/plain'이라고 했다면, 내용이 스크립트처럼 생겼어도 절대 실행하지 않고 텍스트로만 취급한다.
// 컨트롤러에서 정확한 Content-Type 지정
@RestController
class ApiController {
@GetMapping(value = "/api/data", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> data() { return Map.of("msg", "hello"); }
}
// 전역 nosniff 적용 (스프링 시큐리티 기본 제공)
http.headers(h -> h.contentTypeOptions(withDefaults())); // X-Content-Type-Options: nosniff
Spring의 대응 전략 2 - CSP (Content-Security-Policy)
- CSP는 XSS 방어의 핵심으로, 브라우저에 신뢰할 수 있는 콘텐츠의 출처(whitelist)를 알려주는 정책이다. 이 정책에 어긋나는 모든 리소스는 브라우저가 로드하거나 실행하는 것을 거부한다.
- 스크립트 실행 원천을 제한하고, 인라인 스크립트 금지 또는 허용된 nonce/hash만 실행하게 한다.
Spring Security로 간단 설정 + 동적 nonce 바인딩 예시
http.headers(h -> h
.contentSecurityPolicy(csp -> csp.policyDirectives(
"default-src 'self'; script-src 'self' 'nonce-" + "{{nonce}}" + "'; object-src 'none'; base-uri 'self'"))
);
// {{nonce}}는 필터/인터셉터에서 생성한 값을 템플릿/응답에 치환
- script-src 'self' 'nonce-{{nonce}}' 'strict-dynamic'
- 의미: 스크립트(JavaScript) 파일에 대해서는 더 까다로운 규칙을 적용한다.
- 'self': 우리 도메인에서 온 스크립트는 허용한다.
- 'nonce-{{nonce}}': 서버가 매 요청마다 생성하는 **일회성 비밀번호(nonce)**를 가진 <script> 태그만 실행을 허용한다. 공격자가 페이지에 악성 스크립트를 삽입해도, 이 비밀번호를 모르기 때문에 브라우저는 해당 스크립트의 실행을 차단한다.
세션 고정
세션 고정(Session Fixation) 공격은 공격자가 미리 발급받은 세션 ID를 피해자에게 강제로 사용하게 한 뒤, 해당 세션을 탈취하는 공격 기법이다.
- 즉, 피해자가 정상적으로 로그인하더라도 이미 공격자가 알고 있는 세션 ID가 그대로 유지된다면 공격자는 세션을 가로챌 수 있다.

Spring의 대응 전략 - session fixation 보호 전략
Spring Security는 로그인 시점에 세션 ID를 어떻게 다룰지 결정하는 session fixation 보호 전략을 제공한다.
http.sessionManagement(session -> session
.sessionFixation(fixation -> fixation.migrateSession()));

| 전략 | 설명 | 특징 | 권장 사용처 |
| none | 기존 세션 그대로 유지 | 보호 없음 | 테스트 환경, 보안 불필요 서비스 |
| newSession | 새로운 세션을 생성하고 기존 속성은 복사하지 않음 | 세션 완전 초기화 | 극단적 보안이 필요한 경우 |
| migrateSession (기본값) | 새로운 세션을 생성하고 속성을 복사 | 보안 + 사용자 편의 균형 | 일반 웹 서비스 기본 |
| changeSessionId | 세션 ID만 새로 발급, 속성 그대로 유지 | Servlet 3.1+ 환경 권장 | 최신 톰캣/서블릿 컨테이너 |
- 기본값인 migrateSession은 대부분의 서비스에서 충분히 안전하다.
- Servlet 3.1 이상 환경에서는 changeSessionId를 권장한다.
- none은 절대 운영 환경에서 사용하지 않는다!
- 세션 ID 변경 후에도 필요한 속성(예: 장바구니 정보)이 유지되는지 반드시 확인해야 한다.
JWT 탈취
JWT(JSON Web Token) 탈취는 공격자가 다른 사용자의 유효한 JWT를 훔쳐서, 그 사용자인 것처럼 위장하여 시스템에 접근하는 공격이다.
주요 탈취 경로는 다음과 같다:
- XSS(Cross-Site Scripting) 공격: 위에 언급했던 것처럼, 공격자가 웹사이트에 악성 스크립트를 삽입하여, 사용자의 브라우저에 저장된 JWT를 읽어 자신의 서버로 전송하는 방식이다.
- 중간자 공격(MITM): 암호화되지 않은 HTTP 통신을 사용하는 경우, 공격자가 네트워크 중간에서 오가는 데이터를 가로채 JWT를 탈취하는 방식이다.
Spring의 대응 전략 1 - HttpOnly 쿠키 사용
토큰을 안전하게 보관하는 것이 먼저다.
- HttpOnly 속성을 가진 쿠키를 사용해야 한다 -> HttpOnly는 자바스크립트가 쿠키에 접근하는 것을 막는 브라우저 보안 기능인다, 이 설정을 적용하면 XSS 공격이 성공하더라도 공격자는 쿠키에 담긴 JWT를 읽어갈 수 없다. Spring에서는 ResponseCookie를 사용하여 쉽게 설정할 수 있다.
- 또한, 절대 로컬 스토리지(Local Storage)를 사용하면 안된다 -> 로컬 스토리지는 자바스크립트로 쉽게 접근할 수 있어 XSS 공격에 매우 취약하다: 공격자가 삽입한 스크립트 한 줄이면 저장된 모든 토큰이 유출될 수 있다...!
// HttpOnly, Secure, SameSite 속성을 모두 적용한 안전한 쿠키 생성
ResponseCookie cookie = ResponseCookie.from("refreshToken", jwt)
.httpOnly(true)
.secure(true) // HTTPS 환경에서만 쿠키 전송
.sameSite("Lax") // CSRF 공격 방어
.path("/")
.maxAge(쿠키만료시간)
.build();
response.addHeader("Set-Cookie", cookie.toString());
Spring의 대응 전략 2 - 짧은 만료 기간의 액세스 토큰 + 리프레시 토큰 도입
- 액세스 토큰(Access Token) vs 리프레시 토큰(Refresh Token)
- 액세스 토큰(Access Token): 실제 API 요청에 사용되며, 만료 시간을 5분~30분 정도로 매우 짧게 설정한다.
- 이 토큰이 탈취되더라도 공격자는 아주 짧은 시간 동안만 시스템을 악용할 수 있다.
- 리프레시 토큰(Refresh Token): 액세스 토큰이 만료되었을 때 새로운 액세스 토큰을 재발급받는 용도로만 사용된다.
- 만료 시간은 7일~30일 정도로 길게 설정하며, 반드시 HttpOnly 쿠키에 안전하게 저장하여 탈취 위험을 최소화해야 한다.
- 액세스 토큰(Access Token): 실제 API 요청에 사용되며, 만료 시간을 5분~30분 정도로 매우 짧게 설정한다.
Spring의 대응 전략 3 - 모든 통신에 HTTPS 강제
가장 기본적이고 중요하다. HTTPS를 사용하면 클라이언트와 서버 간의 모든 통신이 암호화되므로, 공격자가 네트워크 중간에서 데이터를 가로채는 중간자 공격(MITM)을 원천적으로 차단할 수 있다.
SecurityConfig 예시:
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.requiresChannel(channel ->
channel.anyRequest().requiresSecure()); // 모든 요청에 HTTPS 강제
// ... 기타 설정
return http.build();
}
}'Spring > Spring Security' 카테고리의 다른 글
| [Spring Security] 비동기 환경에서 컨텍스트(MDC, SecurityContext) 전달하기 (0) | 2025.10.16 |
|---|