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)를 활용해 릴리스 전에 검증
⏪ 이전 글 보기: 권한 모델링: ROLE vs 권한 문자열, @PreAuthorize, 도메인 권한
⏩ 다음 글 보기: 리소스 서버 분리 & API Gateway 연동(Spring Cloud Gateway)
⏩ 다음 글 보기: 리소스 서버 분리 & API Gateway 연동(Spring Cloud Gateway)
👉 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 현실 설정
'Java & Spring' 카테고리의 다른 글
| REST API 에러 코드 규격: 문제 상세(Problem Details)로 정리 (0) | 2025.10.18 |
|---|---|
| 리소스 서버 분리 & API Gateway 연동(Spring Cloud Gateway) (0) | 2025.10.15 |
| 권한 모델링: ROLE vs 권한 문자열, @PreAuthorize, 도메인 권한 (0) | 2025.10.14 |
| OAuth2 로그인(Google/GitHub) + JWT 브릿지 (0) | 2025.10.13 |
| JWT 심화: 만료/클레임/키 회전 + 4편과의 연결 전략 (0) | 2025.10.13 |