이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- CORS, CSRF, XSS 사고는 각각 어떤 증거와 containment가 필요한가?
- 브라우저 보안 사고에서 사용자 세션 피해를 어떻게 산정하는가?
- Spring/HTTP 설정과 프론트 배포를 어떤 순서로 점검해야 하는가?
개요
CORS, CSRF, XSS 사고는 모두 브라우저를 거치지만 실패한 방어선은 서로 다르다.
CORS 사고는 다른 Origin의 스크립트가 민감 응답을 읽을 수 있었는지가 핵심이다.
CSRF 사고는 사용자의 쿠키가 자동 전송되어 원하지 않는 상태 변경이 실행되었는지가 핵심이다.
XSS 사고는 공격자가 사용자 브라우저에서 스크립트를 실행해 세션, 화면, 요청을 조작했는지가 핵심이다.
공격 시나리오
- CORS 설정이
Access-Control-Allow-Origin: *와 credential 허용을 함께 열어 민감 응답이 외부 사이트에 노출된다. - 공격자는 피싱 페이지에서 로그인된 사용자의 브라우저로 상태 변경 요청을 보내 CSRF token 없는 endpoint를 실행한다.
- 게시글, 프로필, 관리자 메모 같은 저장 필드에 XSS payload를 심어 다른 사용자의 브라우저에서 실행한다.
- XSS payload는 token 저장소, DOM 내용, 사용자 대신 보내는 API 요청을 노린다.
- 사고 대응이 늦으면 악성 payload가 CDN cache, DB, 검색 색인, 알림 메시지에 남을 수 있다.
취약한 요청/응답 예시
GET /api/me/billing HTTP/1.1
Origin: https://evil.example
Cookie: SESSION=s%3AabcHTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: true
Content-Type: application/json
{
"email": "member@example.com",
"cardLast4": "1111"
}<img src=x onerror="fetch('/api/profile',{method:'POST',credentials:'include',body:'role=admin'})">민감 응답 읽기, 상태 변경 요청, 스크립트 실행은 증거와 대응 방법이 다르다.
주제별 핵심 판단
- CORS는 서버 간 접근 제어가 아니라 브라우저가 응답을 스크립트에 노출할지 정하는 정책이다.
- Credential 포함 CORS가 열려 있으면 읽힌 응답의 민감도를 먼저 평가한다.
- CSRF는 쿠키 기반 인증과 상태 변경 method가 만나는 지점을 본다.
- XSS는 입력 제거보다 이미 저장된 payload 제거, 세션 보호, CSP 강화가 함께 필요하다.
- CSP report는 공격 시도 탐지에 도움을 주지만 XSS 방어의 전부가 아니다.
- SameSite 쿠키는 CSRF 위험을 줄이지만 모든 브라우저 흐름과 OAuth redirect를 자동으로 해결하지 않는다.
개선된 요청/응답 예시
HTTP/1.1 403 Forbidden
Vary: Origin
Content-Type: application/json
X-Request-Id: req-1a77c90e
{
"error": "origin_not_allowed"
}Set-Cookie: SESSION=s%3Aabc; Path=/; Secure; HttpOnly; SameSite=Lax
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'self'; report-to csp-endpointlevel=WARN event=browser.origin.denied origin=https://evil.example path=/api/me/billing credentialed=true result=blocked traceId=req-1a77c90e
level=WARN event=browser.xss.payload_removed location=profile:bio affectedRows=17 action=sanitized traceId=ir-20260701-004방어 설계
- CORS allowlist는 정확한 scheme, host, port 기준으로 관리하고 동적 반사를 금지한다.
- Credential 포함 요청은
Access-Control-Allow-Origin을 와일드카드로 쓰지 않는다. - 상태 변경 요청은 CSRF token, SameSite 쿠키, Origin/Referer 검증을 조합한다.
- XSS 방어는 입력 검증, 출력 인코딩, 안전한 템플릿 사용, CSP를 함께 적용한다.
- 사고 중에는 악성 payload가 저장된 DB, cache, 검색 색인, message queue를 모두 확인한다.
- Token을 브라우저 JavaScript가 읽을 수 있는 저장소에 둘수록 XSS 사고의 영향이 커진다.
- 프론트 배포와 백엔드 CORS 설정 변경은 같은 변경 관리로 추적한다.
- 사용자 세션 무효화는 XSS payload 실행 범위와 token 저장 방식을 기준으로 결정한다.
Spring/HTTP 예시
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'nonce-{nonce}'; object-src 'none'; base-uri 'self'")
)
);
return http.build();
}@Bean
CorsConfigurationSource corsConfigurationSource() {
var config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com", "https://admin.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowCredentials(true);
config.setAllowedHeaders(List.of("Content-Type", "X-CSRF-TOKEN"));
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}설정은 예시일 뿐이며 실제 nonce 삽입, CSRF token 노출 방식, SPA 구조는 애플리케이션 구조에 맞춰 검증해야 한다.
운영 로그와 감사 지점
level=INFO event=incident.browser.scope type=xss storedPayload=true affectedUsers=42 sessionRevoked=42 cspReports=318 traceId=ir-20260701-004
level=WARN event=csrf.validation.failed actor=member:1004 path=/api/email method=POST origin=https://evil.example reason=missing_token traceId=req-922e1f00- CORS 거부 로그에는 origin, path, credential 포함 여부, matched policy를 남긴다.
- CSRF 실패 로그에는 method, path, origin, referer, actor, reason을 남긴다.
- XSS 대응 로그에는 payload 위치, 제거 건수, 영향 사용자, 세션 무효화 여부를 남긴다.
- CSP report는 샘플링과 중복 제거를 하되 report-uri 자체가 민감 데이터를 받지 않게 한다.
- 브라우저 보안 사고는 배포 버전, CDN cache purge, DB 정리 시각을 함께 기록한다.
실전 판단 기준
- CORS 사고는
외부 사이트가 요청을 보냈는가보다응답을 읽을 수 있었는가를 본다. - CSRF 사고는 실제 상태 변경이 실행되었는지 audit log로 확인한다.
- XSS 사고는 payload 제거와 세션 보호를 동시에 진행한다.
- CSP를 추가해도 이미 저장된 payload가 사라지는 것은 아니다.
- SameSite를 Strict로 바꾸면 OAuth/social login redirect나 외부 결제 복귀가 깨질 수 있다.
- 프론트에서 escape를 해도 API 소비자가 여러 개면 서버 출력 맥락을 다시 확인해야 한다.
- 대응 완료는 보안 header 배포가 아니라 악성 payload 제거와 회귀 테스트 통과까지 포함한다.
테스트 포인트
- 허용되지 않은 Origin에서 credential 포함 요청을 보내 응답이 스크립트에 노출되지 않는지 확인한다.
- 상태 변경 endpoint에 CSRF token 없이 POST/PUT/DELETE를 보내 거부되는지 확인한다.
- 저장 필드에 HTML, attribute, URL, JavaScript context별 XSS payload를 넣어 실행 여부를 확인한다.
- CSP report가 들어오지만 정상 기능이 깨지지 않는지 확인한다.
- 악성 payload 제거 후 cache와 검색 색인에 같은 payload가 남지 않았는지 확인한다.
- XSS 영향 사용자 세션 무효화 후 이전 token이 거부되는지 확인한다.
위험 신호!
- Origin 값을 검증 없이 그대로
Access-Control-Allow-Origin에 반사한다. - Credential 포함 CORS endpoint가 민감 응답을 반환한다.
- GET 요청이 상태를 변경하거나 CSRF token 없이 민감 변경을 허용한다.
- 저장형 XSS payload를 DB에서 제거하지 않고 CSP만 추가한다.
- CSP report에 URL query, cookie, token 같은 민감값이 섞인다.
- 브라우저 보안 사고 대응에서 프론트 배포와 백엔드 설정 변경 이력이 연결되지 않는다.
확인 질문
확인 질문
- CORS 사고의 핵심 영향 판단은 무엇인가?
- 외부 Origin 스크립트가 credential 포함 민감 응답을 읽을 수 있었는지다.
- CSRF와 XSS 대응이 다른 이유는 무엇인가?
- CSRF는 사용자의 인증 상태로 원치 않는 요청을 보내는 문제이고, XSS는 브라우저에서 공격자 스크립트가 실행되는 문제이기 때문이다.
- 저장형 XSS 대응에서 payload 제거 외에 해야 할 일은 무엇인가?
- 영향 사용자 세션 보호, cache purge, CSP/출력 인코딩 보강, 회귀 테스트를 함께 해야 한다.