CORS: 교차 출처 리소스 공유 (Cross-Origin Resource Sharing)
- 교차 출처(Cross-Origin): 다른 출처를 말함
- 출처(Origin): Scheme(Protocol) + Host + Port
- 출처가 같으려면, Scheme, Host, Port가 같아야 한다.
- Port는 생략 가능하다. HTTP, HTTPS 프로토콜 기본 포트 넘버가 정해져 있기 때문에 http Scheme에서 Port가 생략되면 80번 포트라고 가정한다. ⇒ 정책에 따라 다른 출처로 자원을 요청하면 CORS 오류가 발생할 수 있다.
- 출처 비교 로직은 브라우저에 구현되어 있다. 서버 CORS 정책에 위배되지 않아서 응답을 보내주었더라도 브라우저가 CORS 정책에 위반된다고 판단하면 응답을 버릴 수도 있음 ⇒ 이 경우 서버 로그에는 정상 응답 로그만 찍힌다
- 적용되는 리소스
- XMLHttpRequest, Fetch API 호출
- 웹 폰트 (
@font-face
) - WebGL 텍스처
- drawImage()로 캔버스에 그린 이미지/비디오 프레임
- 이미지로부터 추출하는 CSS Shapes
CORS 정책
SOP (Same-Origin Policy)
- 같은 출처에서만 리소스를 공유할 수 있다 [RFC 6454]
- Scheme, host, port 모두 일치
- 예외 조항에 해당하는 리소스 요청은 출처가 달라도 허용한다.
- CORS 정책을 지킨 리소스 요청
CORS
- 기본적으로 자신의 출처와 동일한 리소스만 불러올 수 있다.
- 다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 한다.
쓰는 이유
- 브라우저 - 서버 간의 안전한 데이터 요청 및 전송을 지원하기 위함
- 웹 클라이언트 어플리케이션은 사용자 공격에 매우 취약하다
- DOM 정보, 리소스 출처, 서버와의 통신 내용을 모두 열람할 수 있음
- 자바스크립트 난독화가 되어 있다 하더라도 절대로 이해할 수 없게 되어있는 것은 아님
- 다른 출처 어플리케이션을 제한하지 않으면, 다른 사람이 코드를 심어서 사용자 정보를 탈취 가능
- CSRF(Cross-Site Request Forgery), XSS(Cross-Site Scripting) 등
CORS의 동작
CORS 동작 방식에는 세 가지의 시나리오가 있다
- Preflight Request
- Simple Request
- Credentialed Request
CORS 에러 경험해보기
- 개발자 도구를 열고, Console 탭에서 아래 코드 입력
1const headers = new Headers({2'Content-Type': 'text/xml',3});4fetch('https://github.com/ooooorobo', { headers });
기본 흐름
- 클라 ⇒ 서버: HTTP 요청 헤더 Origin 필드에 요청 출처를 담는다
- 서버 ⇒ 클라: HTTP 응답 헤더 Access-Control-Allow-Origin 필드에 이 리소스에 접근이 허용된 출처를 담아서 보낸다
- 브라우저는 요청의 Origin과 응답의 Access-Control-Allow-Origin을 비교하고 응답의 유효 여부를 결정한다.
- 응답이 유효하지 않다고 판단하면 CORS 에러가 발생한다.
개발자 도구 콘솔 탭과 네트워크 탭에서 CORS error 메시지를 볼 수 있다.
1. Preflight Request
- 브라우저가 요청을 보낼 때, 본 요청 이전에 예비 요청(Preflight)을 전송
- Preflight 요청: HTTP 메소드 중 Options 메소드를 사용
- 트래픽 증가 우려 → 서버 헤더 설정으로 캐싱 가능
Access-Control-Request-*
형태 헤더Access-Control-Request-Method
: 본 요청에서 사용할 메소드Access-Control-Request-Headers
: 본 요청에서 전송할 헤더 종류
- 클라 ⇒ 서버: 본 요청을 보내려는 URI에 Option 메소드(Preflight) 요청을 보낸다.
1// 본 요청2const headers = new Headers({3'Content-Type': 'text/xml',4});5fetch('https://roborobo.tistory.com', { headers });
- Preflight 헤더에는 본 요청 메소드 종류, 사용할 헤더 등의 정보를 담음
1...2Access-Control-Request-Headers: content-type3Access-Control-Request-Method: GET4Connection: keep-alive5Host: roborobo.tistory.com6Origin: https://www.naver.com7Referer: https://www.naver.com/8...
- 서버 ⇒ 클라: 서버의 CORS 정책에 따라 Preflight에 대해 메소드, 헤더, 쿠키 등의 허용 여부에 대해 응답
1Access-Control-Allow-Origin: https://roborobo.tistory.com2Content-Encoding: gzip3Content-Length: 50604Content-Type: text/html; charset=utf-85Date: Thu, 28 Oct 2021 05:08:23 GMT6P3P: CP='ALL DSP COR MON LAW OUR LEG DEL'7Vary: Accept-Encoding8X-UA-Compatible: IE=Edge
- 응답에서 요청을 허용했다면 본 요청을 전송
2. Simple Request
- 과정
- 클라 ⇒ 서버: 예비 요청을 보내지 않고 바로 본 요청을 보낸다.
- 서버 ⇒ 클라: 응답 헤더에
Access-Control-Allow-Origin
값 등을 보낸다. - 브라우저가 CORS 정책 위반 여부를 검사한다.
- 예비 요청을 생략하기 위해서는 특정 조건을 만족해야 한다. (매우 드문 경우임)
- 요청 메소드가
GET, HEAD, POST
중 하나 Accept
,Accept-Language
,Content-Language
,Content-Type
,DPR
,Downlink
,Save-Data
,Viewport-Width
, **Width
**를 제외한 헤더를 사용하면 안된다.- 만약 **
Content-Type
**를 사용하는 경우에는application/x-www-form-urlencoded
,multipart/form-data
, **text/plain
**만 허용된다.
3. Credentialed Request
credentials
옵션- 요청에 인증과 관련된 정보를 담을 수 있게 한다.
- 기본적으로
XMLHttpRequest
,fetch API
는 브라우저 쿠키 정보나 인증 관련 헤더를 요청에 담지 않는다. - 값의 종류
same-origin
(default): 같은 출처 간 요청만 인증 정보를 담음include
: 모든 요청에 인증 정보를 담음omit
: 모든 요청에 인증 정보를 담지 않음credentials: include
사용 시에는 브라우저가 조건을 추가로 검사한다.include
모드에서는 요청의Access-Control-Allow-Origin
헤더에*
를 사용할 수 없고, 명시적 URL이어야 한다.- 응답 헤더에
Access-Control-Allow-Credentials: true
가 있어야 한다.
CORS 해결 방법
- 동일 출처 사용하기
- 서버에서 Access-Control-Allow-Origin 세팅
- 와일드카드
*
사용하기 ⇒ 보안 위험 - 허용할 출처의 이름을 명시해주기
- 프록시 서버 사용
- 클라 - 서버 사이에서 헤더를 추가하거나 요청을 허용/거부하는 역할
- 로컬에서 개발 시,
http://localhost:3000
과 같은 주소를 사용하게 되는데, 서버에서 이와 같은 범용 주소를Access-Control-Allow-Origin
에 잘 넣어주지 않음 - Webpack Dev Server 세팅
1module.exports = {2 devServer: {3 proxy: {4 '/api': {5 target: 'https://domain.com',6 changeOrigin: true,7 pathRewrite: { '^/api': '' },8 },9 }10 }11}1213// 혹은 package.json에서14{15 ...16 "proxy" : "https://api.domain.com"17 ...18}