HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
👻
개발 기록
/
📚
CS 스터디
/
📚
SOP, CORS
📚

SOP, CORS

CORS의 정의를 알고 왜 요청이 실패하는지, 어떤 방식으로 요청하는지 알아보자.
notion image
 

💡 CORS의 배경과 필요성

  • 도메인에 보내는 HTTP 요청에 대해 브라우저는 해당 도메인과 연결된 모든 HTTP 쿠키를 연결함.
  • 이는 인증 및 세션 설정에 특히 유용함.

다음의 상황을 가정해보자!

⭐
접근을 막는건 API를 요청하는 사이트도, API를 가진 사이트도 아닌 접속한 사이트를 보여주고 있는 브라우저임!
notion image
notion image
  • facebook-clone.com에 로그인한다고 가정해보자.
  • 브라우저는 facebook-clone과 관련된 session cookie를 저장할 것임.
  • 이제 facebook-clone을 방문할 때마다 저장된 session cookie를 이용하여 HTTP 요청을 하기 때문에 다시 로그인할 필요가 없어짐.

하지만 facebook-clone에 다른 요청을 보낸다면?

  • 브라우저는 다른 요청이 있을 때, facebook-clone에 대해 저장된 쿠키를 보낼 수도 있음.
  • 만약 갑작스런 팝업이 떠 실수로 evil-site.com으로 이동했다면?
notion image
  • evil-site는 facebook-clone.com/api로 요청을 보낼 수 있음.
  • 요청이 facebook-clone으로 보내지기 때문에 브라우저는 관련 쿠키를 포함하여 응답을 함.
  • evil-site는 session cookie를 보내고 facebook-clone에 대한 인증된 액세스 권한을 얻음.
  • cross-site request으로 성공적으로 계정이 해킹되었다!

그냥 다른 도메인의 요청은 전부 막자!

  • 출처를 엄격하게 비교하여(출처가 완벽히 같아야) 데이터를 응답해주기로 결정.
  • 이를 SOP(Same Origin Policy)라고 함.
  • SOP란 다른 출처의 리소스를 사용하는 것에 제한하는 보안 방식임.
💡
출처란? URL의 Scheme, host, port를 통해 같은 출처인지 아닌지 판단할 수 있음.
notion image
  • Scheme, host, port 하나라도 다르면 다른 출처라고 판단함. 그러니 같은 출처임을 보여주려면 위 3개의 값이 같아야 함.
  • 이는 브라우저가 판단하기 때문에 브라우저마다 기준이 다를 수 있음. (ex. IE는 port를 비교하지 않음)
💡
잠깐 퀴즈! http://localhost와 동일출처인 URL은? 1. https://localhost 2. http://localhost:80 3. http://127.0.0.1 4. http://localhost/api/cors

한계는 없을까?

  • 다른 웹사이트의 유용한 API를 주고 받을 수 없음.
  • 과거엔 JSONP라는 트릭을 사용하여 다른 출처임에도 우회하여 사용할 수 있도록 했지만, 보안을 철저히 하고자 했던 SOP의 목적과 달리 우회하여 보안을 다시 악화시키기 때문.
  • 이러한 문제에 대한 해결책으로 CORS가 생겨남.
 

❓ CORS란?

CORS(Cross-Origin Resource Sharing)는 추가 HTTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수있는 권한을 부여하도록 브라우저에 알려주는 체제.
 

🗣️ CORS의 요청 종류

⭐
CORS는 브라우저에서 출처를 비교하고 판단함.

Simple request (단순 요청)

  • 한 번의 요청과 응답을 주고 받아 출처를 비교함.
  • 대신 안전성을 보장할 수 있도록 다음의 조건을 지정.
    • GET이나 POST, HEAD를 사용한 요청.
    • 다음 목록에 속하는 헤더로 요청.
      • Accept
      • Accept-Language
      • Content-Language
      • 값이 application/x-www-form-urlencoded이나 multipart/form-data, text/plain인 Content-Type

요청을 보내보자!

  • CORS 요청을 보낼 경우 브라우저는 항상 Origin이라는 헤더를 요청에 추가함.
GET /request Host: anywhere.com Origin: https://javascript.info ...
https://javascript.info/page에서 https://anywhere.com/request에 요청을 보낸다고 가정할 때 헤더의 형태.
  • 서버는 요청 헤더에 있는 Origin를 검사하고, 요청을 받아들이기로 동의한 상태라면 특별한 헤더 Access-Control-Allow-Origin를 응답에 추가함.
  • 이 헤더엔 허가된 오리진(위 예시에선 https://javascript.info)에 대한 정보나 *이 명시됨.
  • 이때 응답 헤더 Access-Control-Allow-Origin에 오리진 정보나 *이들어있으면 응답은 성공하고 그렇지 않으면 응답이 실패함.
  • 이 과정에서 브라우저는 중재인의 역할을 함.
notion image
  1. 브라우저는 크로스 오리진 요청 시 Origin에 값이 제대로 설정, 전송되었는지 확인함.
  1. 브라우저는 서버로부터 받은 응답에 Access-Control-Allow-Origin이 있는지를 확인해서 서버가 CORS 요청을 허용하는지 아닌지를 확인함.
  1. 응답 헤더에 Access-Control-Allow-Origin이 있다면 자바스크립트를 사용해 응답에 접근할 수 있고 아니라면 에러가 발생함.
200 OK Content-Type:text/html; charset=UTF-8 Content-Length: 12345 API-Key: 2c9de507f2c54aa1 Access-Control-Allow-Origin: https://javascript.info Access-Control-Expose-Headers: Content-Length,API-Key
응답 형태 예시.
⭐
응답 헤더
  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma
⚠️Access-Control-Expose-Headers라는 헤더를 보내야 전달할 수 있는 목록이 있음. ex) Content-Length, API-Key
 

Preflight request (사전 요청)

  • 과거엔 웹페이지에서 GET, POST 이외의 HTTP 메서드를 사용해 요청을 보낼 수 있을거란 상상조차 할 수 없었음.
  • GET, POST 이외의 메서드를 사용한 요청이 오면 '이건 브라우저가 보낸 요청이 아니야’라고 판단하고 접근 권한을 확인함.
  • 이런 혼란스러운 상황을 피하고자 preflight 요청이라는 사전 요청을 서버에 보내 권한이 있는지를 확인함.
  • preflight 요청은 OPTIONS 메서드를 사용하고 두 헤더가 함께 들어감.
    • Access-Control-Request-Method 헤더 – preflight 요청에서 사용하는 메서드 정보가 담겨있음.
    • Access-Control-Request-Headers 헤더 – preflight 요청에서 사용하는 헤더 목록이 담겨있음. 각 헤더는 쉼표로 구별.
  • preflight 요청을 허용하기로 협의했다면, 상태 코드가 200인 응답을 다음과 같은 헤더와 함께 브라우저로 보냄.
    • Access-Control-Allow-Origin –  *이나요청을 보낸 Origin(ex. https://javascript.info)
    • Access-Control-Allow-Methods – 허용된 메서드 정보가 담겨있음.
    • Access-Control-Allow-Headers – 허용된 헤더 목록이 담겨있음.
    • Access-Control-Max-Age – permission 체크 여부를 몇 초간 캐싱해 놓을지를 명시함. 그럼 브라우저는 일정 기간 동안 preflight 요청을 보낼 수 있음.
notion image
🤔
두 번씩 요청하는 이유? 브라우저가 거부하기 전 서버가 처리하는 엉뚱한 결과를 방지하기 위함.

요청을 보내보자!

let response = await fetch('https://site.com/service.json', { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'API-Key': 'secret' } });
PATCH 메서드를 사용해 요청을 보냄.
  • 위 요청이 preflight 요청인 이유 3가지.
    • PATCH 메서드를 사용.
    • Content-Type이 application/x-www-form-urlencoded나 multipart/form-data, text/plain가 아님.
    • 비표준 헤더 API-Key를 사용함.
//1단계(preflight 요청) OPTIONS /service.json Host: site.com Origin: https://javascript.info Access-Control-Request-Method: PATCH Access-Control-Request-Headers: Content-Type,API-Key //2단계(preflight 응답) 200 OK Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Methods: PUT,PATCH,DELETE Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control Access-Control-Max-Age: 86400 //3단계(실제 요청) PATCH /service.json Host: site.com Content-Type: application/json API-Key: secret Origin: https://javascript.info //4단계(실제 응답) Access-Control-Allow-Origin: https://javascript.info
응답 형태 예시.
  • 이 모든 과정이 끝나야 자바스크립트를 사용해 실제 응답을 읽을 수 있음.
 

🔐 자격증명

  • 일반적으로 브라우저는 쿠키나 HTTP 인증 같은 자격 증명(credential)에 대해 매우 민감하다고 생각하기 때문에 함부로 보내주지 않음.
  • 그럼에도 불구하고 서버에서 이를 허용하고 싶다면, 자격 증명이 담긴 헤더를 명시적으로 허용하겠다는 세팅을 서버에 해줘야 함.
  • fetch 메서드에 자격 증명 정보를 함께 전송하려면 다음과 같이 credentials: "include" 옵션을 추가하면 됨.
fetch('http://another.com', { credentials: "include" });
  • 옵션을 추가하면 fetch로 요청을 보낼 때 another.com에 대응하는 쿠키가 함께 전송됨.
  • 자격 증명 정보가 담긴 요청을 서버에서 받아들이기로 동의했다면 서버는 응답에 Access-Control-Allow-Origin 헤더와 함께 Access-Control-Allow-Credentials: true 헤더를 추가해서 보냄.
200 OK Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Credentials: true
  • 자격 증명이 함께 전송되는 요청을 보낼 땐 Access-Control-Allow-Origin에 *을쓸 수 없음.
  • 위 예시에서처럼 Access-Control-Allow-Origin엔 정확한 오리진 정보만 명시되어야 함.
  • 이런 제약이 있어야 어떤 오리진에서 요청이 왔는지에 대한 정보를 서버가 신뢰할 수 있기 때문.
 
 
참고자료 :
[10분 테코톡] 🌳 나봄의 CORS
CORS를 처음 마주하는 분들에게
CORS
3 Ways to Fix the CORS Error — and How the Access-Control-Allow-Origin Header Works