Spring Security를 활용한 웹 보안 강화: CSRF, CORS, CSP 전략

CSRF 공격 방어 메커니즘

사이트 간 요청 위조(CSRF)는 인증된 사용자의 세션을 악용해 무단 작업을 수행하려는 공격 기법이다. 예를 들어 사용자가 로그인 상태일 때 악성 사이트에서 의도하지 않은 송금이나 설정 변경 요청이 자동으로 전송될 수 있다. 이를 막기 위해선 요청의 진위를 검증하는 추가적인 보안 계층이 필요하다.

CSRF 원리와 대응 방안

공격은 다음과 같은 흐름으로 진행된다:

  1. 사용자가 A사이트에 로그인하고 쿠키 기반 세션이 유지된다.
  2. 사용자가 악성 B사이트를 방문하면, 해당 페이지 내 숨겨진 폼 또는 스크립트가 A사이트로 요청을 전송한다.
  3. 브라우저는 자동으로 A사이트의 쿠키를 포함시켜 요청을 보내며, 서버는 이를 정상 요청으로 간주할 수 있다.
  4. 결과적으로 사용자 몰래 계정 정보 수정, 결제 등 민감 작업이 실행된다.

이러한 위험을 차단하기 위한 주요 방법은 다음과 같다:

  • 토큰 기반 검증: 서버가 고유한 비밀 토큰을 생성하고 클라이언트가 후속 요청 시 이 값을 함께 제출하도록 한다. 서버는 일치 여부를 확인한다.
  • 쿠키-헤더 일치 검사: 동일한 값의 토큰을 쿠키와 요청 본문(또는 헤더)에 모두 저장하고, 두 값이 동일한지 비교한다. 크로스 도메인에서는 쿠키 읽기가 불가능하므로 공격자는 이를 우회하기 어렵다.
  • 커스텀 HTTP 헤더: AJAX 요청 시 X-Requested-With와 같은 임의 헤더를 포함하면, 브라우저의 동일 출처 정책에 의해 타 도메인에서 요청을 재현하기 어려워진다.
  • Referer 헤더 검증: 요청의 출처 도메인이 신뢰 가능한지 확인하지만, 프라이버시 설정으로 인해 누락되거나 변조될 수 있어 단독 사용은 권장되지 않는다.

Spring Boot 환경에서의 CSRF 보호 구현

Spring Security는 기본적으로 CSRF 방어를 활성화하며, 개발자는 이를 쉽게 확장하거나 커스터마이징할 수 있다.

기본 설정 예시

먼저 보안 설정 클래스에서 CSRF 보호를 명시적으로 구성한다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .csrf().disable(); // 특정 상황에서만 비활성화 (주의!)
    }
}

실제 운영 환경에서는 대부분 활성화 상태로 유지해야 하며, 일부 API 엔드포인트에서만 예외 처리를 고려한다.

템플릿 엔진과의 통합

Thymeleaf를 사용할 경우, 아래와 같이 폼 내에 토큰이 자동 삽입된다.

<form th:action="@{/submit}" method="post">
  <input type="text" name="data" />
  <button type="submit">전송</button>
  <!-- Thymeleaf 자동 생성: <input type="hidden" name="_csrf" value="..." /> -->
</form>

AJAX 요청 처리

JavaScript 기반 요청에서는 메타 태그에서 토큰을 추출하여 헤더에 포함시킨다.

<meta name="_csrf" content="${_csrf.token}" />
<meta name="_csrf_header" content="${_csrf.headerName}" />

// JS 코드
const token = document.querySelector('meta[name="_csrf"]').getAttribute('content');
const header = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');

fetch('/api/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    [header]: token
  },
  body: JSON.stringify({ value: 'test' })
});

CORS 정책 관리

다른 도메인에서의 자원 접근을 허용하기 위해 CORS(Cross-Origin Resource Sharing)를 적절히 설정해야 한다. 잘못된 설정은 보안 취약점을 유발할 수 있으므로 세심한 주의가 필요하다.

전역 설정

애플리케이션 전체에 적용할 CORS 규칙을 정의할 수 있다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors(c -> {
                CorsConfigurationSource source = request -> {
                    CorsConfiguration config = new CorsConfiguration();
                    config.setAllowedOrigins(List.of("https://trusted-site.com"));
                    config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT"));
                    config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
                    config.setAllowCredentials(true);
                    return config;
                };
                c.configurationSource(source);
            })
            .authorizeRequests().anyRequest().authenticated();
    }
}

컨트롤러 수준 설정

특정 엔드포인트에 대해 더 세밀한 제어가 필요한 경우, 애너테이션을 사용한다.

@RestController
@RequestMapping("/api")
public class ApiController {

    @CrossOrigin(
        origins = "https://trusted-site.com",
        methods = {RequestMethod.GET},
        maxAge = 3600
    )
    @GetMapping("/data")
    public ResponseEntity<String> getData() {
        return ResponseEntity.ok("Success");
    }
}

내용 보안 정책(CSP) 적용

CSP는 XSS 공격을 방지하기 위한 강력한 도구로, 어떤 출처의 스크립트나 리소스가 로드될 수 있는지를 선언적으로 제어한다.

기본 CSP 설정

Spring Security를 통해 응답 헤더에 CSP 정책을 추가할 수 있다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .headers()
            .contentSecurityPolicy("default-src 'self'; script-src 'self' https://cdn.example.com; img-src 'self' data:;")
        .and()
        // 기타 설정...
}

이 정책은 기본적으로 모든 리소스를 동일 출처에서만 허용하며, 스크립트는 지정된 외부 CDN에서만 로드 가능하게 한다.

CSP 위반 보고 기능

정책 위반이 발생했을 때 이를 수집하기 위해 보고 URI를 지정할 수 있다.

.contentSecurityPolicy("default-src 'self'; report-uri /csp-violation-report;")

수신 컨트롤러는 다음과 같이 구현한다.

@PostMapping("/csp-violation-report")
public ResponseEntity<Void> handleCspReport(@RequestBody String payload) {
    // 위반 로그 기록 또는 분석 시스템 전달
    log.warn("CSP 위반 감지: {}", payload);
    return ResponseEntity.accepted().build();
}

동적 보안 전략

정적 설정만으로는 부족한 경우, 요청 컨텍스트에 따라 보안 정책을 동적으로 조정할 수 있다.

사용자 역할 기반 CSP

관리자와 일반 사용자에게 서로 다른 스크립트 로딩 권한을 부여하는 필터 예시:

@Component
public class DynamicCspFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
        throws IOException, ServletException {
        
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String role = extractUserRole(request); // 실제 로직 구현 필요
        String policy = role.equals("ADMIN") ? 
            "script-src 'self' 'unsafe-inline'" : 
            "script-src 'self'";

        response.setHeader("Content-Security-Policy", policy);
        chain.doFilter(req, res);
    }
}

분산 환경에서의 CSRF 토큰 관리

여러 인스턴스를 운영할 경우, Redis와 같은 외부 저장소에 토큰을 공유할 수 있다.

public class RedisCsrfTokenRepository implements CsrfTokenRepository {
    private final StringRedisTemplate redisTemplate;

    public RedisCsrfTokenRepository(StringRedisTemplate template) {
        this.redisTemplate = template;
    }

    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", UUID.randomUUID().toString());
    }

    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        String key = "csrf:" + request.getSession().getId();
        if (token == null) {
            redisTemplate.delete(key);
        } else {
            redisTemplate.opsForValue().set(key, token.getToken(), Duration.ofMinutes(30));
        }
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        String key = "csrf:" + request.getSession().getId();
        String token = redisTemplate.opsForValue().get(key);
        return token != null ? new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token) : null;
    }
}

이러한 전략들은 각각의 보안 요구사항에 맞게 조합되어 사용되며, 과도한 보안은 UX를 해칠 수 있고, 부족한 보안은 시스템을 노출시키므로 균형 잡힌 설계가 중요하다.

태그: Spring Security CSRF CORS CSP Web Security

5월 22일 20:19에 게시됨