Access to fetch at ‘https://api.lubycon.com/me’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
Cross-Origin Resource Sharing
의 줄임말이다. 서버의 위치를 의미하는 https://google.com
과 같은 URL들은 여러 개의 구성 요소로 이루어져있다.
이때 출처는 Protocol
과 Host
, 그리고 위 그림에는 나와있지 않지만 :80
, :443
과 같은 포트 번호까지 모두 합친 것을 의미한다. 즉, 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 것들을 합쳐놓은 것이다.
또한 출처 내의 포트 번호는 생략이 가능한데, 이는 각 웹에서 사용하는 HTTP
, HTTPS
프로토콜의 기본 포트 번호가 정해져있기 때문이다. HTTP
가 정의된 RFC 2616
문서를 보면 다음과 같이 기본 포트 번호가 함께 정의되어있는 것을 볼 수 있다.
3.3.2 http URL
…
If the port is empty or not given, port 80 is assumed. The semantics are that the identified resource is located at the server listening for TCP connections on that port of that host, and the Request-URI for the resource is abs_path (section 5.1.2).
…
브라우저의 개발자 도구의 콘솔에서 Location
객체가 가지고 있는 origin
프로퍼티에 접근함으로써 손 쉽게 어플리케이션이 실행되고 있는 출처를 알아낼 수도 있다.
console.log(location.origin);
"https://evan-moon.github.io"
CORS
, 그리고 또 한 가지는 SOP(Same-Origin Policy)
이다.SOP
는 지난 2011년, RFC 6454
에서 처음 등장한 보안 정책으로 말 그대로 “같은 출처에서만 리소스를 공유할 수 있다”라는 규칙을 가진 정책이다.CORS
라는 이름이 처음 등장한 것은 2009년이라, SOP
의 등장보다 빠르다)Access to network resources varies depending on whether the resources are in the same origin as the content attempting to access them.
Generally, reading information from another origin is forbidden. However, an origin is permitted to use some kinds of resources retrieved from other origins. For example, an origin is permitted to execute script, render images, and apply style sheets from any origin. Likewise, an origin can display content from another origin, such as an HTML document in an HTML frame. Network resources can also opt into letting other origins read their information, for example, using Cross-Origin Resource Sharing.
RFC 6454 - 3.4.2 Network Access
SOP
정책을 위반한 것이 되고, 거기다가 SOP
의 예외 조항인 CORS
정책까지 지키지 않는다면 아예 다른 출처의 리소스를 사용할 수 없게 되는 것이다.SOP
에서 정의된 예외 조항과 CORS
를 사용할 수 있는 케이스들이 맞물리지 않을 경우에는 아예 리소스 요청을 할 수 없는 케이스도 존재할 수 있다.<script>
태그 안에 날 것 그대로의 소스 코드가 떡하니 노출되는 사이트들도 많다.CSRF(Cross-Site Request Forgery)
나 XSS(Cross-Site Scripting)
와 같은 방법을 사용하여 사용자의 어플리케이션에서 코드가 실행된 것처럼 꾸며서 사용자의 정보를 탈취하기가 너무나도 쉬워진다.Scheme
, Host
, Port
, 이 3가지가 동일하면 된다.https://heayounchoi.github.io:80
라는 출처를 예로 들면 https://
이라는 스킴에 heayounchoi.github.io
호스트를 가지고 :80
번 포트를 사용하고 있다는 것만 같다면 나머지는 전부 다르더라도 같은 출처로 인정이 된다는 것이다.RFC 6454
의 Comparing Origins
섹션에는 “만약 출처가 스킴/호스트/포트의 삼중 체계라면…” 이라는 전제가 붙어있기 때문에 포트 번호의 경우 어떻게 해석하냐에 따라 구현이 달라질 수 있다. 이런 경우에는 각 브라우저들의 독자적인 출처 비교 로직을 따라가게 된다.CORS
정책을 위반하는 리소스 요청을 하더라도 해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아니라면 서버는 정상적으로 응답을 하고, 이후 브라우저가 이 응답을 분석해서 CORS
정책 위반이라고 판단되면 그 응답을 사용하지 않고 그냥 버리는 순서인 것이다.CORS
는 브라우저의 구현 스펙에 포함되는 정책이기 때문에, 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않는다. 또한 CORS
정책을 위반하는 리소스 요청 때문에 에러가 발생했다고 해도 서버 쪽 로그에는 정상적으로 응답을 했다는 로그만 남기 때문에, CORS
가 돌아가는 방식을 정확히 모르면 에러 트레이싱에 난항을 겪을 수도 있다.HTTP
프로토콜을 사용하여 요청을 보내게 되는데, 이때 브라우저는 요청 헤더에 Origin
이라는 필드에 요청을 보내는 출처를 함께 담아보낸다.Access-Control-Allow-Origin
이라는 값에 “이 리소스를 접근하는 것이 허용된 출처”를 내려주고, 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin
과 서버가 보내준 응답의 Access-Control-Allow-Origin
을 비교해본 후 이 응답이 유효한 응답인지 아닌지를 결정한다.CORS
가 동작하는 방식은 한 가지가 아니라 세 가지의 시나리오에 따라 변경되기 때문에 요청이 어떤 시나리오에 해당되는지 잘 파악한다면 CORS
정책 위반으로 인한 에러를 고치는 것이 한결 쉬울 것이다.프리플라이트(Preflight)
방식은 일반적으로 웹 어플리케이션을 개발할 때 가장 자주 마주치는 시나리오이다. 이 시나리오에 해당하는 상황일 때 브라우저는 요청을 한번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송한다.Preflight
라고 부르는 것이며, 이 예비 요청에는 HTTP
메소드 중 OPTIONS
메소드가 사용된다. 예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것이다.fetch API
를 사용하여 브라우저에게 리소스를 받아오라는 명령을 내리면 브라우저는 서버에게 예비 요청을 먼저 보내고, 서버는 이 예비 요청에 대한 응답으로 현재 자신이 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 응답 헤더에 담아서 브라우저에게 다시 보내주게 된다.CORS
정책을 위반했다고 판단하고 CORS
에러를 뱉게 되는 것이다.CORS
정책 위반 여부를 검사하기도 한다.단순 요청은 예비 요청을 보내지 않고 바로 서버에게 본 요청부터 때려박은 후, 서버가 이에 대한 응답의 헤더에 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
만 허용된다.
저 조건에 명시된 헤더들은 진짜 기본적인 헤더들이기 때문에, 복잡한 상용 웹 어플리케이션에서 이 헤더들 외에 추가적인 헤더를 사용하지 않는 경우는 드물다. 당장 사용자 인증에 사용되는 Authorization
헤더 조차 저 조건에는 포함되지 않는다.
게다가 대부분의 HTTP API
는 text/xml
이나 application/json
컨텐츠 타입을 가지도록 설계되기 때문에 사실 상 이 조건들을 모두 만족시키는 상황을 만들기는 그렇게 쉽지 않은 것이 현실이다.
CORS
의 기본적인 방식이라기 보다는 다른 출처 간 통신에서 좀 더 보안을 강화하고 싶을 때 사용하는 방법이다.XMLHttpRequest
객체나 fetch API
는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다. 이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 credentials
옵션이다.옵션 값 | 설명 |
---|---|
same-origin (기본값) | 같은 출처 간 요청에만 인증 정보를 담을 수 있다 |
include | 모든 요청에 인증 정보를 담을 수 있다 |
omit | 모든 요청에 인증 정보를 담지 않는다 |
same-origin
이나 include
와 같은 옵션을 사용하여 리소스 요청에 인증 정보가 포함된다면, 이제 브라우저는 다른 출처의 리소스를 요청할 때 단순히 Access-Control-Allow-Origin
만 확인하는 것이 아니라 좀 더 빡빡한 검사 조건을 추가하게 된다.CORS
정책 위반 여부를 검사하는 룰에 다음 두 가지를 추가하게 된다.
Access-Control-Allow-Origin
에는*
를 사용할 수 없으며, 명시적인 URL이어야한다.- 응답 헤더에는 반드시
Access-Control-Allow-Credentials: true
가 존재해야한다.
CORS
정책 위반으로 인한 문제를 해결하는 가장 대표적인 방법은, 그냥 정석대로 서버에서 Access-Control-Allow-Origin
헤더에 알맞은 값을 세팅해주는 것이다.*
을 사용하여 이 헤더를 세팅하게 되면 모든 출처에서 오는 요청을 받아먹겠다는 의미이므로 당장은 편할 수 있겠지만, 바꿔서 생각하면 정체도 모르는 이상한 출처에서 오는 요청까지 모두 받아먹겠다는 오픈 마인드와 다를 것 없으므로 보안적으로 심각한 이슈가 발생할 수도 있다.Nginx
나 Apache
와 같은 서버 엔진의 설정에서 추가할 수도 있지만, 아무래도 복잡한 세팅을 하기는 불편하기 때문에 소스 코드 내에서 응답 미들웨어 등을 사용하여 세팅하는 것이 좋다. Spring
, Express
, Django
와 같이 이름있는 백엔드 프레임워크의 경우에는 모두 CORS
관련 설정을 위한 세팅이나 미들웨어 라이브러리를 제공하고 있으니 세팅 자체가 어렵지는 않다.CORS
를 가장 많이 마주치는 환경은 바로 로컬에서 프론트엔드 어플리케이션을 개발하는 경우라고 해도 과언이 아니다. 백엔드에는 이미 Access-Control-Allow-Origin
헤더가 세팅되어있겠지만, 이 중요한 헤더에다가 http://localhost:3000
같은 범용적인 출처를 넣어주는 경우는 드물기 때문이다.webpack-dev-server
를 사용하여 자신의 머신에 개발 환경을 구축하게 되는데, 이 라이브러리가 제공하는 프록시 기능을 사용하면 아주 편하게 CORS
정책을 우회할 수 있다.module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.heayounchoi.com',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
}
}
}
/api
로 시작하는 URL로 보내는 요청에 대해 브라우저는 localhost:8000/api
로 요청을 보낸 것으로 알고 있지만, 사실 뒤에서 웹팩이 https://api.heayounchoi.com
으로 요청을 프록싱해주기 때문에 마치 CORS
정책을 지킨 것처럼 브라우저를 속이면서도 원하는 서버와 자유롭게 통신을 할 수 있다. 즉, 프록싱을 통해 CORS
정책을 우회할 수 있는 것이다.webpack-dev-middleware
와 Node
서버의 조합으로 개발 환경을 직접 구축했더라도 http-proxy-middleware
라이브러리를 사용하면 손쉽게 프록시 설정을 할 수 있다. (webpack-dev-server
도 내부적으로는 어차피 http-proxy-middleware
를 사용한다)webpack-dev-server
가 구동하는 환경이 아니기 때문에 프록싱이고 나발이고 이상한 곳으로 API 요청을 보내기 때문이다.CORS
정책을 지킨 요청을 제외한 SOP
의 예외 조항은 실행 가능한 스크립트, 렌더될 이미지, 스타일 시트 정도가 있다.CORS
를 우회할 수 있을까? <img src="https://heayounchoi.tistory.com/rss">
<script src="https://heayounchoi.tistory.com/rss"></script>
CORS
를 위반하지 않고 요청 자체는 성공한다. 그리고 브라우저의 개발자 도구의 네트워크 탭에서 이 요청들의 헤더를 자세히 살펴보면 Sec-Fetch-Mode: no-cors
라는 값이 포함되어 있는 것을 볼 수 있다.Sec-Fetch-Mode
헤더는 요청 모드를 설정하는 필드인데, 브라우저는 이 필드의 값이 no-cors
인 경우에는 다른 출처라고 해도 CORS
정책 위반 여부를 검사하지 않는다. 하지만 한 가지 슬픈 점은 브라우저가 이 헤더에 값이 포함된 요청의 응답을 자바스크립트에게 알려주지 않는다는 것이다. 즉, 코드 레벨에서 절대 이 응답에 담긴 내용에 접근할 수가 없다.CORS
정책 위반으로 인해 생기는 문제를 해결할 때 가장 번거로운 점은 문제를 겪는 사람과 문제를 해결해야하는 사람이 다르다는 것이다.CORS
정책은 브라우저의 구현 스펙이기 때문에 정책 위반으로 인해 문제를 겪는 사람은 대부분 프론트엔드 개발자이지만, 정작 문제를 해결하기 위해서는 백엔드 개발자가 서버 어플리케이션의 응답 헤더에 올바른 Acccess-Control-Allow-Origin
이 내려올 수 있도록 세팅해줘야 한다.