본문 바로가기
Java & Spring

CORS·XSS·헤더 보안: SPA/REST 현실 설정

by yamoojin83 2025. 10. 14.

CORS·XSS·헤더 보안: SPA/REST 현실 설정

프런트엔드가 분리된 SPA 아키텍처에서는 CORS보안 헤더가 기본입니다. 여기에 CSP(Content-Security-Policy)로 스크립트 로드를 통제하고, 템플릿/응답에서 XSS를 예방해야 합니다. 이 글은 Spring Security 6 기준으로 실제 운영에서 무난하게 쓰는 설정을 정리합니다.

1) CORS: 정확한 Origin만 허용


@Configuration
public class CorsCfg {

  @Bean
  CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration c = new CorsConfiguration();
    // 정확한 오리진만 명시(와일드카드 X). 여러 개면 리스트로 추가.
    c.setAllowedOrigins(List.of("https://app.example.com", "https://admin.example.com"));
    c.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
    c.setAllowedHeaders(List.of("Authorization","Content-Type","X-Requested-With"));
    c.setExposedHeaders(List.of("Location")); // 필요 시
    c.setAllowCredentials(true); // 쿠키/인증정보 포함 요청 허용 시 true
    c.setMaxAge(3600L);
    UrlBasedCorsConfigurationSource s = new UrlBasedCorsConfigurationSource();
    s.registerCorsConfiguration("/**", c);
    return s;
  }
}

@Configuration
@EnableMethodSecurity
public class SecurityCfg {
  @Bean
  SecurityFilterChain http(HttpSecurity http) throws Exception {
    http.cors(Customizer.withDefaults())     // 위 Bean 사용
        .csrf(csrf -> csrf.disable())        // API는 보통 Stateless + CSRF 비활성
        .authorizeHttpRequests(a -> a.anyRequest().authenticated())
        .oauth2ResourceServer(o -> o.jwt()); // 예시
    return http.build();
  }
}

주의: allowCredentials=true이면 allowedOrigins*를 쓸 수 없습니다.

2) 보안 헤더: Spring Security로 한 번에


@Configuration
public class HeadersCfg {

  @Bean
  SecurityFilterChain headersOnly(HttpSecurity http) throws Exception {
    http
      .headers(h -> h
        .contentSecurityPolicy(csp -> csp
          .policyDirectives("default-src 'self'; object-src 'none'; " +
                            "img-src 'self' data:; frame-ancestors 'none';"))
        .referrerPolicy(rp -> rp
          .policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
        .httpStrictTransportSecurity(hsts -> hsts
          .includeSubDomains(true).preload(true).maxAgeInSeconds(31536000))
        .frameOptions(fo -> fo.sameOrigin()) // H2 콘솔 등 필요 시만 유지
        .xssProtection(x -> x.disable())     // 표준화된 CSP로 대체
        .contentTypeOptions(Customizer.withDefaults()) // X-Content-Type-Options: nosniff
      );
    return http.build();
  }
}
  • CSP: 인라인 스크립트 차단이 핵심. 꼭 필요하면 nonce 전략을 사용하세요.
  • HSTS: HTTPS 강제. 프리로드 등록 전 전체 서브도메인이 HTTPS 준비됐는지 확인.
  • nosniff: MIME 스니핑 방지. Spring Security 기본값으로 활성화됩니다.

3) CSP Nonce(선택): 템플릿 + 필터

요청마다 난수 nonce를 생성해 응답 헤더와 스크립트 태그에 함께 넣습니다.


@Component
public class CspNonceFilter extends OncePerRequestFilter {
  @Override
  protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
      throws ServletException, IOException {
    String nonce = Base64.getEncoder().encodeToString(SecureRandom.getSeed(16));
    req.setAttribute("cspNonce", nonce);
    res.setHeader("Content-Security-Policy",
        "default-src 'self'; script-src 'self' 'nonce-" + nonce + "'; object-src 'none'");
    chain.doFilter(req, res);
  }
}

<!-- Thymeleaf 예시 -->
<script th:nonce="${cspNonce}">/* inline script */</script>

4) 서버 템플릿 XSS 기본기

  • escape 출력: th:text / JSP의 <c:out> 사용. th:utext는 신중히.
  • 사용자 입력 HTML 허용 시: OWASP Java HTML Sanitizer 같은 화이트리스트 필터 사용.
  • 쿼리스트링/헤더를 그대로 화면에 반영하지 않기(로그만 남김).

5) 프록시(Nginx)에서 보안 헤더 보강


server {
  listen 443 ssl http2;
  server_name api.example.com;

  add_header Content-Security-Policy "default-src 'self'; object-src 'none'; img-src 'self' data:;" always;
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
  add_header Permissions-Policy "geolocation=()" always;

  location / {
    proxy_pass http://app:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-For $remote_addr;
  }
}

6) CSRF와 CORS의 경계

  • API가 Stateless + JWT라면 보통 CSRF 비활성이 타당합니다.
  • 세션 기반 폼 로그인이라면 CSRF 토큰을 유지하고, CORS는 필요 경로에만 적용.

7) 운영 체크

  • 교차 도메인 요청이 꼭 필요한지, 허용 Origin 목록이 최신인지 주기적 점검
  • 프런트 빌드 파이프라인에서 nonce/CSP 위배 여부 테스트
  • 브라우저 콘솔의 CSP 보고서(Report-To/Report-Only)를 활용해 릴리스 전에 검증

 

 

👉 1편: SecurityFilterChain 완전정복: 요청 매칭/인가 룰 제대로 이해하기

👉 2편: PasswordEncoder와 회원가입: BCrypt·Pepper·비밀번호 정책

👉 3편: AuthenticationProvider & UserDetailsService 커스터마이징

👉 4편: 세션 기반 로그인 vs Stateless: CSRF/Remember-me/세션관리

👉 5편: JWT 심화: 만료/클레임/키 회전 + 4편과의 연결 전략

👉 6편: OAuth2 로그인(Google/GitHub) + JWT 브릿지

👉 7편: 권한 모델링: ROLE vs 권한 문자열, @PreAuthorize, 도메인 권한

👉 8편: CORS·XSS·헤더 보안: SPA/REST 현실 설정

👉 9편: 리소스 서버 분리 & API Gateway 연동(Spring Cloud Gateway)

👉 10편: 감사 로깅/Audit: 로그인 실패·권한거부 탐지 규격