[Web] CORS
1. SOP
먼저 Cors에 대해 알아보기 전에 알아야 하는 개념인 SOP에 대해 먼저 설명해보자면
SOP는 Same-Origin Policy의 약자로 우리나라 말로하면 동일 출처 정책 이라는 뜻이다 즉 동일 출처 정책 이라는말 그대로 다른 출처의 리소스를 사용하는 것에 대해 제한하는 보안방식 이다
1.1 출처(origin)
그렇다면 출처 라는건 웹에서 어떤걸 말하는 걸까? 😕
위 그림에서 보다시피 출처란 프로토콜, 호스트이름, 포트를 합친 것을 출처라고 한다
이 출처를 통해서 같은 출처인지 다른 출처인지를 판단한다!
그렇다면 http://localhost와 동일 출처인 url은 어떤것일까?
1. https://localhost
2. http://localhost:80
3. http://127.0.0.1
4. http://localhost/api/cors
정답은 2번과 4번이다. 얼핏 보기엔 호스트 이름이 똑같고 127.0.0.1 또한 localhost의 주소인데 다 동일출처인게 아닌가 싶지만
- 1번은 원본 url의 프로토콜이 http, 보기의 프로토콜이 https이므로 동일출처로 판단하지 않는다
- 2번은 원본 url에는 80번 포트번호가 명시되어있지 않지만 http 프로토콜 자체가 기본적으로 80포트에서 통신하므로 원본 url에서는 단순히 생략되어있는것과 마찬가지라 같은 출처로 판단한다
- 3번은 localhost의 ip주소가 루프백ip인 127.0.0.1 이지만 브라우저가 출처를 판단할때 호스트이름의 문자열을 비교해서 동일 출처인지 아닌지를 판단하기때문에 127.0.0.1 과 localhost는 다르므로 같은 출처가 아니다
- 4번은 뒤에 붙어있는 /api/cors가 로케이션 주소이므로 그 /api 전까지 붙어있는 호스트이름만 비교해서 같은 출처로 판단한다.
1.2 왜 SOP를 사용하나?
SOP는 예를 들어 http://example1.com 에서 로드된 JS 코드가 http://example2.com 에 있는 정보를 요청하거나 수정하려고 시도한다고 할때 기본적으로 차단한다
먼저 웹 페이지가 다른 출처에서 가져온 스크립트를 자신에게 접근할수 있도록 허용한다면 그 스크립트가 사용자의 데이터를 읽거나 변경할 수 있게 된다
예를 들면 Bank.com 이라는 은행사이트에 본인의 계좌가 있다고 한다면 브라우저에서 Bank.com 에 접속한 상태로
브라우저의 다른탭에서 fishing.com이라는 사이트에 접속하게 되면 fishing.com에서 접속했을때 악성 스크립트를 받아서 실행하게 되면 fishing.com 에서 받아서 실행한 스크립트가 Bank.com의 본인 계좌에 접근 할 수 있게 되어서 돈을 훔치거나 하는 등의 동작을 할 수 있게된다.
이를 방지하기 위해서 SOP를 사용해 Bank.com에 접근 가능한 스크립트는 Bank.com 에서 받은 스크립트만 접근이 가능하도록 해서 이런 공격을 막아 낼 수 있다.
2. CORS
그렇다면 다른 출처의 리소스를 필요로 할때는 어떻게 해야할까?
이때 사용하는 것이 CORS (Cross-Origin Resouce Sharing) 이다 다른 출처의 자원을 공유한다는 뜻이다.
2.1 CORS란?
CORS는 추가 HTTP 헤더를 사용해서 한 출처에서 실행 중인 웹 어플리케이션이 다른 출처의 선택적인 자원에 접근 할 수 있게 권한을 부여하도록 브라우제 알려주는 정책이다
2.2 CORS 접근제어 방법
2.2.1. 단순 요청 (Simple Request)
사전 요청 없이 바로 요청을 날리는 방법
단순 요청은 다음 조건을 모두 만족해야 한다
- GET,POST,HEAD 메서드 중에 하나여야 한다.
- Contnet-Type의 경우 application/x-www-form-urlencoded, multipart/form-data, text/plain 중에 하나여야 한다.
- 헤더의 경우 Accept, Accept-Language, Content-Language, Content-Type 만 허용된다.
2.2.2. 사전 요청 (Preflight Request)
OPTIONS 메서드를 통해서 다른 도메인의 리소스에 요청이 가능한지 확인하는 작업이다
사전 요청을 보낼때는 담아보내야 하는 값들이 있다
- Origin ----> 지금 이 요청을 어디서 보내는지에 대한 출처가 있어야한다
- Access-Control-Request-Method ---> 어떤 메서드 요청을 보낼건지 담아서 서버에 이런 요청이 갈건데 괜찮니? 라고 물어본다
- Access-Control-Request-Headers ---> 실제 요청에는 어떤 헤더들이 담겨 있는지 명시해준다
아래 값들을 담아서 서버에 사전 요청을 하면 서버에서 응답을 해준다
Access-Control-Allow-Origin: https://foo.example ---> 서버의 허가 출처
Access-Control-Allow-Methods: POST, GET, OPTIONS ---> 서버의 허가 메서드
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type ---> 서버의 허가 헤더
Access-Control-Max-Age: 86400 ---> preflight 응답의 캐시 기간
캐싱을 하는 이유는 매 요청을 보낼때 마다 preflight 를 보내게 되면 리소스적인 측면에서 좋지 않으므로
preflight 응답을 캐싱해놨다가 다음 요청을 보낼땐 캐싱된 요청을 보고 바로 본요청을 보내게 된다.
preflight 응답이 가져야하는 특징으로는 아래와 같은 것들이 있다.
1. 응답 코드는 200대여야 한다
2. 응답 바디는 비어있는것이 좋다
그렇다면 이 사전 요청이라는것을 왜 보내야 할까?🤔
1. 만약 큰 데이터를 담아서 보내는 요청이 있다고 할때 이 요청을 보냈더니 CORS정책을 위반하는 요청이라면 그냥 버려지게 되고 리소스를 낭비한 꼴이 되어버린다 따라서 이를 방지하기 위해 예비요청을 날려 이게 유효한 요청인지 아닌지를 확인 하는 것이다.
2. Preflight가 왜 있는가에 대해서는 쉽게 말해서 CORS spec이 생기기 이전에 만들어진 서버들은 브라우저의 SOP(same origin policy) request만 가능하다는 가정하에 만들어졌는데, cross-site request가 CORS로 인해서 가능해졌기 때문에 이런 서버들은 cross-site request에 대한 보안정책이 없다보니 보안적으로 문제가 생길수 있어 이런 서버들을 보호하기 위해 CORS spec에 preflight request를 포함했다 Preflight request로 서버가 CORS를 인식하고 핸들할수있는지 먼저 확인을 함으로써 CORS를 인식하지 못하는 서버들을 Preflight request를 통해 보호 할 수 있다.
3. Credentialed Request
인증 관련 헤더를 포함할때 사용하는 요청
쿠키를 보내기 위해 클라이언트에서
axios.post(
'https://example.com:1234/users/login',
{ profile: { username: username, password: password } },
{ withCredentials: true }
)
fetch(url, { credentials: 'include' })
아래 코드와 같이 axios를 사용할때 withCredentials 옵션을 true로 설정하거나 fetch api를 사용할때 credentials를 include로 설정하고 서버측에서도 Access-Control-Allow-Credentials 옵션을 true로 설정해줘야 인증 관련 헤더를 받았을때 cors오류가 나지 않는다.
🚨 주의 사항
서버에서 Access-Control-Allow-Credentials 옵션을 true로 설정했을때 주의해야할 점이 있다.
위의 옵션을 활성화 했을때
- Access-Control-Allow-Origin 에 와일드카드 (*) 말고 정확한 출처를 명시 해주어야 한다!
- Access-Control-Allow-Methods 에 와일드카드 (*) 말고 정확한 메서드를 명시 해주어야 한다!
- Access-Control-Allow-Headers 에 와일드카드 (*) 말고 정확한 헤더를 명시 해주어야 한다!
이를 지키지 않으면 CORS 에러가 발생하게 된다.
다만 Simple Request의 경우에는 Access-Control-Allow-Methods, Access-Control-Allow-Headers 헤더는 없어더 괜찮다.
4. CORS 해결하는 방법
CORS 오류가 발생했을때 해결하는 방법으로는 아래와 같은 것들이 있다.
1. 프론트에서 프록시 서버 설정 : 대표적으로 vite 같은 경우 이런식으로 프록시 설정을 해준다
예를 들어, 클라이언트가 localhost:3000에서 실행되고 있고, 프록시 설정에 따라 /api 경로로 들어오는 모든 요청을 http://other-domain.com으로 리디렉션한다고 가정하면
클라이언트에서 /api/some-endpoint로 요청을 보낼 때 이 요청은 localhost:3000/api/some-endpoint로 가는 것으로 인식된다 그러나 실제로는 이 요청이 개발 서버의 프록시를 거쳐 http://other-domain.com/api/some-endpoint로 전달된다.
여기서 중요한 점은 클라이언트 입장에서 볼 때 이 요청은 동일 출처(Same-Origin)인 것으로 인식되므로 CORS 오류가 발생하지 않는다.
하지만 이 방법은 주로 로컬 개발 환경에서만 사용되고, 실제 배포 환경에서의 CORS 문제 해결 방법은 아니다 실제 배포 환경에서는 서버 측에서 적절한 CORS 헤더를 설정하여 문제를 해결해야 한다.
2. 서버에서 CORS 헤더 설정하기: 가장 일반적인 해결 방법은 서버에서 적절한 CORS 헤더를 설정하는 방법
Access-Control-Allow-Origin 헤더에 요청을 보낼 수 있는 도메인을 지정할 수 있다.
3. JSONP 사용하기: JSONP(JSON with Padding)는 웹 페이지가 다른 도메인의 데이터에 접근할 수 있는 방식
JSONP 요청은 <script> 태그를 이용해 이루어지므로 Same Origin Policy에 영향을 받지 않는다
기본적으로 웹 브라우저의 보안 정책인 동일 출처 정책(Same-Origin Policy) 때문에, 웹 페이지는 자신과 같은 출처에서만 데이터를 요청할 수 있다 하지만 <script> 태그는 이런 제한을 받지 않는다 즉, <script> 태그를 사용하면 다른 도메인에서도 스크립트를 로드하고 실행할 수 있다. 하지만 보안적으로 악용의 여지가 있어서 대부분 CORS 설정 통해 CORS 문제를 해결한다.