[SB] 31. CORS

최재원's avatar
Jun 10, 2025
[SB] 31. CORS

1. origin과 remote Address

notion image
notion image
  • DNS 서버가 2개인 이유 → 1개가 오류가 발생할 때 다른 DNS 서버에 요청해야 하니까
Referer 레퍼럴
  • 로그인 할 때 login 으로 post 요청이 오는데 직전 주소 login-form 에서 오는 것이 정상이기 때문
origin 출처
  • blog.com 내 웹사이트 주소가 아닌 곳에서 요청이 오면 정상이 아님
  • user-agent 가 브라우저가 아니면 정상이 아님
CSRF 토큰
  • form 안에 내가 만든 토큰을 넣어서 요청이 올 때 검증 할 수 있다

✅ 정리 및 설명:

  1. Referer (레퍼러)
      • 설명: 현재 요청이 어디에서 왔는지를 알려주는 HTTP 헤더입니다.
      • 예시: 사용자가 /login-form에서 로그인 버튼을 눌러 /login으로 POST 요청을 보낼 경우, Referer/login-form이 됩니다.
      • 보안 용도: 예상한 페이지에서 온 요청인지 확인하는 데 사용할 수 있지만, 이 값은 클라이언트 측에서 조작 가능하므로 완전한 신뢰는 금물입니다.
  1. Origin (출처)
      • 설명: 요청을 보낸 페이지의 프로토콜 + 도메인 + 포트 정보를 담은 헤더입니다.
      • 예시: https://blog.com
      • 보안 용도: 사이트 간 요청 위조(CSRF) 방지 등에서 유효 출처인지 판단할 때 유용합니다.
      • 주의: OriginCORS 요청이나 POST 요청에만 포함되며, 모든 요청에 들어오지는 않습니다.
  1. User-Agent
      • 설명: 요청을 보낸 클라이언트(브라우저, 앱, 봇 등)의 정보를 담은 헤더입니다.
      • 보안 용도: 브라우저가 아닌 자동화된 툴(예: curl, bot 등)에서 요청이 왔는지 탐지할 수 있습니다.
      • 주의: 이것도 조작이 가능하므로 보조 지표로만 활용해야 합니다.
  1. CSRF Token
      • 설명: 서버가 발급한 고유 토큰을 <form> 안에 넣고, 요청 시 함께 보내서 서버에서 검증하는 방식입니다.
      • 보안 용도: CSRF 공격을 방어하는 가장 확실한 방법입니다.
      • 장점: Referer, Origin은 조작 가능하거나 조건부이지만, CSRF 토큰은 서버가 생성한 값을 검증하므로 신뢰성이 높습니다.

🔐 Spring Security에서의 CSRF 방어

1. CSRF란?

CSRF (Cross-Site Request Forgery)
: 사용자의 인증 정보를 악용해 의도하지 않은 요청을 서버에 보내는 공격.

예시 상황

  • 사용자가 bank.com에 로그인한 상태에서
  • 공격자가 만든 악성 사이트에 접속
  • <form action="https://bank.com/transfer" method="POST"> 자동 실행
  • 사용자의 쿠키(세션 정보)를 이용한 의도치 않은 송금 요청이 발생

2. Spring Security의 기본 동작

Spring Security는 기본적으로 POST, PUT, DELETE, PATCH 요청에 대해 CSRF 토큰 검사를 활성화합니다.

🔧 자동 설정

  • 로그인한 사용자의 세션에 CSRF 토큰이 생성됩니다.
  • 이 토큰을 HTML <form> 내부에 숨겨진 필드로 넣어야 합니다.
<input type="hidden" name="_csrf" value="${_csrf.token}" />

자바로 http 요청하는 방법

코드
import org.springframework.http.*; import org.springframework.web.client.RestTemplate; import java.util.*; public class RestTemplateCsrfExample { public static void main(String[] args) { String targetUrl = "https://example.com/submit"; // RestTemplate 객체 생성 RestTemplate restTemplate = new RestTemplate(); // ======================== // 1. 요청 헤더 설정 // ======================== HttpHeaders headers = new HttpHeaders(); // (1) CSRF 토큰 - 서버에서 받은 값이어야 함 headers.set("X-CSRF-TOKEN", "AbCdEfGhIj123456"); // 수동 입력 // (2) JSESSIONID - 서버에서 받은 쿠키값이어야 함 headers.set("Cookie", "JSESSIONID=abcde12345xyz"); // (3) Referer headers.set("Referer", "https://example.com/form"); // (4) User-Agent headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); // (5) Content-Type headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // ======================== // 2. 요청 바디 설정 // ======================== MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); body.add("amount", "1000"); body.add("to", "receiver_account"); // ======================== // 3. HttpEntity 만들기 // ======================== HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers); // ======================== // 4. 요청 실행 // ======================== ResponseEntity<String> response = restTemplate.exchange( targetUrl, HttpMethod.POST, requestEntity, String.class ); // ======================== // 5. 응답 출력 // ======================== System.out.println("Response Code: " + response.getStatusCode()); System.out.println("Response Body: " + response.getBody()); } }
자바로 하는 fetch 요청은 가능하다
자바스크립트로 fetch 요청하는 것을 원천적으로 막는 이유는
게시판 글에 스크립트가 있으면 내가 작성한 스크립트가 아닌데 의도하지 않게 내가 보낸 것처럼 되기 때문에 막아야 하는 것이다
같은 출처는 내가 만든 사이트에서 포함되어있는 내용이기 때문에 내가 조절할 수 있다
내가 분석 가능한 요청이기 때문
하지만 다른 출처에서 오는 스크립트는 내가 조정할 수 없는 내용이기 때문에 그냥 막는다
 

2. CORS

SOP 에 의해 막혀버리는 요청을 받기 위한 정책
SOP 는
자바스크립트 요청만 막는다
postman, java 등 다른 방법은 막지 않는다
최소한 자바스크립트로 장난질 하는 것을 막기 위함이다
notion image
notion image
notion image

🔐 CORS란 (Cross-Origin Resource Sharing)

CORS는 브라우저에서 보안상의 이유로 다른 출처의 리소스 요청을 제한하는 SOP(Same-Origin Policy)를 완화시키기 위한 메커니즘입니다.
기본적으로 브라우저는 현재 페이지의 출처와 다른 출처로 요청이 가는 것을 차단합니다. 하지만 API 서버와 프론트엔드 서버가 다른 도메인을 사용하는 경우, 출처가 다르기 때문에 브라우저는 이를 막게 되죠.
그래서 CORS는 서버가 특정 출처를 명시적으로 허용함으로써 이 제한을 풀 수 있도록 해주는 기능입니다.

📌 예를 들어,

  • 브라우저에서 http://localhost:3000 프론트앱이 실행 중이고
  • http://api.example.com/data로 요청을 보낸다면
브라우저는 두 출처가 다르므로 요청을 차단합니다. 이때 서버가 아래와 같은 헤더를 포함한 응답을 보내면 브라우저는 차단하지 않습니다.
Access-Control-Allow-Origin: http://localhost:3000

📎 요약하면,

  • CORS는 브라우저의 보안 정책을 우회하기 위한 안전한 방법
  • 요청 자체는 서버까지 가지만, 브라우저가 응답을 읽지 못하게 막는 방식
  • 서버가 허용한 출처(origin)에 한해 브라우저가 응답을 허용함
  • 서버 응답에 필요한 CORS 관련 헤더를 명시해야 함

🌍 SOP란 (Same-Origin Policy)

SOP는 웹 브라우저에서 기본적으로 작동하는 가장 중요한 보안 정책 중 하나입니다. 이 정책은 서로 다른 출처의 자바스크립트가 데이터를 공유하거나 접근하지 못하도록 차단합니다.

🔐 출처(origin)란?

  • 프로토콜 (http, https)
  • 호스트 (example.com)
  • 포트 번호 (3000, 8080 등)
이 세 요소 중 하나라도 다르면 브라우저는 출처가 다르다고 간주합니다.

📌 예를 들어,

  • https://example.com:443http://example.com:80은 다른 출처
  • https://example.comhttps://api.example.com도 다른 출처

🔥 왜 필요한가?

만약 SOP가 없다면, 악성 사이트가 사용자가 로그인된 다른 사이트에 자바스크립트 요청을 보내고, 해당 응답을 가져와서 세션이나 개인정보를 탈취할 수 있습니다.
예를 들어, 사용자가 https://bank.com에 로그인한 상태에서 악성 스크립트가 https://bank.com/account에 요청하고, 응답을 읽을 수 있다면 심각한 보안 위협이 됩니다.
SOP는 이러한 위험을 근본적으로 차단하는 역할을 합니다.

✈️ Preflight란

Preflight는 CORS의 보조 개념으로, **브라우저가 민감하거나 복잡한 요청을 보낼 때, 실제 요청 전에 서버에 허용 여부를 먼저 묻는 요청(OPTIONS 메서드)**입니다. 이 요청을 통해 브라우저는 "내가 이런 방식으로 요청을 보내도 되냐?"고 미리 물어보는 것입니다.

📌 언제 Preflight가 발생하나요?

  • 요청 메서드가 PUT, DELETE, PATCH인 경우
  • 요청에 Authorization, Content-Type: application/json 같은 커스텀 헤더가 있는 경우
  • 요청에 쿠키, 토큰 등이 포함되는 경우

📫 Preflight 과정

  1. 브라우저가 먼저 OPTIONS 요청을 서버에 보냅니다.
  1. 서버가 Access-Control-Allow-* 헤더를 포함하여 허용한다는 응답을 보냅니다.
  1. 브라우저가 그제서야 본 요청 (예: PUT, DELETE)을 보냅니다.

📄 예시 요청

OPTIONS /data HTTP/1.1 Origin: http://localhost:3000 Access-Control-Request-Method: PUT Access-Control-Request-Headers: Authorization, Content-Type

📄 예시 응답

HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: PUT, GET, DELETE Access-Control-Allow-Headers: Authorization, Content-Type

✅ 핵심 요약

  • Preflight는 서버가 민감한 요청을 허용하는지 사전 확인하는 절차
  • 브라우저가 보안을 위해 자동으로 처리
  • 서버가 200 OK로 응답하지 않거나 허용 헤더가 없으면 본 요청은 차단됨

실습

1. origin 이 다른 브라우저로 요청

html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <button onclick="join()">회원가입</button> <script> async function join() { let reqBody = { username: "asdf", password: "1234", email: "asdf@nate.com", }; await fetch("http://localhost:8080", { method: "post", body: JSON.stringify(reqBody), headers: { "Content-Type": "application/json", }, }); } </script> </body> </html>

브라우저

notion image
notion image
Access to fetch at 'http://localhost:8080/' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains the invalid value ''. Have the server send the header with a valid value.
  • cors 에러 발생
 

2. 서버에서 origin 을 허용했을 때

spring

package shop.mtcoding.blog._core.filter; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import java.io.IOException; @Slf4j public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; String origin = request.getHeader("Origin"); log.debug("origin : " + origin); response.setHeader("Access-Control-Allow-Origin", ""); // response.setHeader("Access-Control-Expose-Headers", "Authorization"); // 이 헤더 응답을 자바스크립트로 접근 할 수 있도록 허용할지 response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, DELETE, OPTIONS"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Key, Content-Type, Accept, Authorization"); // 서버가 요청을 받을 때 이 헤더값은 허용해서 받는다 // X-key -> 내가 임의로 만든 헤더 값은 X- 를 붙여서 만든다. 약속이다 response.setHeader("Access-Control-Allow-Credentials", "true"); // 쿠키의 세션값을 허용 // 위의 응답 헤더값을 브라우저가 읽어서 // Preflight 요청을 허용하고 바로 응답하는 코드 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(200); } else { chain.doFilter(req, res); } } }
  • Access-Control-Allow-Origin → 허용할 origin 주소 설정
  • Access-Control-Allow-Methods → 서버가 이러한 메서드로 오는 자바스크립트 요청은 받겠다 설정
  • Access-Control-Expose-Headers → 이 헤더 응답을 자바스크립트로 접근 할 수 있도록 허용할지
  • Access-Control-Max-Age → Preflight 요청 결과를 브라우저가 캐시할 시간. 해당 시간 동안 브라우저는 동일한 자바스크립트 요청에 대해 options 요청을 보내지 않는다
  • Access-Control-Allow-Headers", "Origin, X-Key, Content-Type, Accept, Authorization → 자바스크립트로 서버에 보낼 수 있는 헤더 지정
    • X-Key: 사용자 정의 헤더 예시 (커스텀 헤더는 X- 접두사를 붙이는 관례)
  • Access-Control-Allow-Credentials → 쿠키 같은 자격 정보(세션, 토큰)를 포함한 요청 허용 여부
    • 자바스크립트 ajax 요청이 올 때 요청 내부에 자격 정보를 담아 오는 것을 허용 하겠는가?
  • "OPTIONS".equalsIgnoreCase(request.getMethod()) → 브라우저가 보낸 options 요청이면? 응답을 하고 아니면 내부 로직 실행

브라우저

notion image
notion image
  • 서버에서 허용한 값들을 브라우저가 확인하고 post 요청을 보낸다
notion image
  • 브라우저에서 차단 하지 않고 성공 하였다
Share article

jjack1