이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 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%3Aabc
HTTP/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-endpoint
level=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/출력 인코딩 보강, 회귀 테스트를 함께 해야 한다.

참고 문서