CORS
악명 높은 CORS를 정리해보자
Cross-Origin-resource-sharing 문제로 서로 다르출처에 대한 에러이다. 정확히는 SOP를 우회하기 위한 방법이라고 볼 수 있다.
요청 방식에 따른 CORS 발생 여부
모든 요청에서 CORS가 발생하는 것은 아니다.
외부 리소스에 대한 접근이 많은 태그에 경우 Cross-Origin
정책을 지원하기도 한다.
<img>, <video>, <script>, <link>
이 태그들을 기본적으로 Corss-Origin을 지원한다.
XMLHttpRequest, Fetch API 스크립트
기본적으로 Same-Origin 정책이 적용된다.
- 다른 도메인의 소스에 대해 자바스크립트 ajax 요청 API 호출시
- 웹 폰트 CSS 파일 내 @font-face 에서 다른 도메인의 폰트 사용시
이런 경우들을 말한다고 하는데 폰트 얘기는 잘 모르겠다.
JS에서의 요청은 기본적으로 서로 다른 도메인에 대한 요청을 보안상 제한한다
origin 이란
Protocol, Host, Port 까지의 URL을 Origin이라고 한다.
즉, protocol, host, port 세개가 모두 같아야 브라우저는 같은 출처라고 인식한다.
same-origin-policy
말 그대로 동일한 origin에 대한 정책을 말한다.
즉, SOP에 의해서 브라우저는 동일한 출처에서만 리소스를 공유할 수 있다
SOP이 필요한 이유
출처에 대한 제약이 없으면 CSRF(Cross-Site Request Forgery) 나 XSS(Cross-Site Scripting) 등에 쉽게 노출될 수 있다.
SOP 정책이 없는 경우 발생할 수 있는 상황
- 사용자가 악성 사이트에 접속
- 해커가 심어놓은 JS가 실행되면서 사용자가 모르는 사이에 포털 사이트에 API 요청
- 포털 사이트에서 브라우저의 쿠키를 이용하여 로그인 등 상호작용
origin을 비교하여 error를 내뿜는 것은 브라우저가 하는 것이다.
서버에서 헤더 정보를 덜 줘서 발생할 수 있지만, 결국 SOP는 브라우저에서 작용한다.
브라우저를 통하지 않고 서버 간에 통신을 할 때는 SOP 정책이 적용되지 않는다.
즉, 클아이언트가 아닌 서버 단에서 cross-origin의 서버로 API 요청을 하면 CORS 에러로부터 자유로워 진다.
인터넷에서 다른 출처의 리소스를 가져오는 것은 매우 자주 발생하고 중요하기에 이를 전부 차단 할 수가 없다.
그렇기 때문에 우리가 애 먹는 CORS가 등장한 것이다.
CORS는 사실 cross-origin에서 리소스 요청을 부분적으로 허용하기 위한 방법이다.
CORS
Cross-Origin Resource Sharing은 단어 그대로 다른 출처의 리소스 공유에 대한 허용/비허용 정책이다.
앞서 말한 것 처럼 SOP 정책을 위반해도 CORS 정책을 따르면 다른 출처의 리소스라도 허용하겠다는 뜻이다.
브라우저의 CORS 동작
1. 클라이언트에서 HTTP 요청 헤더에 Origin 담아 전달
브라우저는 요청 헤더 중 Origin 이라는 필드에 출처를 담아서 보낸다.
2. 서버는 응답헤더에 Acess-Control-Allow-Origin을 담아서 클라이언트로 전달
서버는 요청에 대한 응답을 할 때 응답 헤더에 Access-Control-Allow-Origin
이라는 필드를 추가하고 값으로 이 응답 리소스에 접근하는 것이 허용된 출처 URL을 내려보낸다.
즉, 이 응답 리소스를 볼 수 있는 origin을 명시하는 것.
3. 클라이언트에서 Origin과 서버가 보내준 Acess-Control-Allow-Origin 비교
- 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin 과 서버가 보내준 응답의 ACAO 를 비교해서 차단할지 말지를 결정
- 만약 유효하지 않으면, 응답 버린다 -> CORS 에러 발생
결론 : CORS 해결책은 서버의 허용이 필요하다.
결론적으로 CORS를 위해서 서버에서 Acess-Control-Allow-Origin
헤더에 허용할 출처를 기재해서 클아이언트에 응답하면 된다.
CORS 작동방식 3가지 시나리오
CORS가 동작하는 방식은 3가지 시나리오에 따라서 변경된다.
예비 요청(preflight request)
브라우저는 요청을 보낼 때 바로 보내지 않고 예비 요청을 보내 서버와 잘 통신되는지 확인한 후 본 요청을 보낸다.
예비 요청는 HTTP 메소드를 GET이나 POST가 아닌 OPTIONS 라는 요청으로 보낸다.
- JS의 fetch() 메서드로 리소스 받아오려고 할 때
- 브라우저가 서버에 HTTP OPTIONS 메서드로 예비요청
- Origin 헤더에 자신의 출처
- Access-Control-Request-Method 헤더에 실체 요청에 사용할 메서드
- Access-Control-Request-Headers 헤더에 실제 요청에 사용할 헤더
- 서버는 예비 요청에 대한 응답으로 어떤 것을 허용하고 어떤것을 금지하고 있는지에 대한 헤더 정보를 담아서 브라우저로 보낸다.
- Access-Control-Allow-Origin 헤더에 허용되는 Origin들의 목록을 설정
- Access-Control-Allow-Methods 헤더에 허용되는 메소드들의 목록을 설정
- Access-Control-Allow-Headers 헤더에 허용되는 헤더들의 목록을 설정
- Access-Control-Max-Age 헤더에 해당 예비 요청이 브라우저에 캐시 될 수 있는 시간을 초 단위로 설정
- 브라우저는 보낸 요청과 서버가 응답해준 정책을 비교하여, 해당 요청이 안전한지 확인하고 본 요청을 보내게 된다
- 서버가 본 요청에 대한 응답을 하면 최종적으로 이 응답 데이터를 자바스립트로 넘겨준다.
개발자 도구에서 예비요청 확인
둘이 다르면 CORS 정책 위반으로 에러를 내뱉는다.
예비 요청의 문제점과 캐싱
예비 요청도 결국에 API 호출이기에 요청 시간이 늘어나고 서버 요청이 많아지는 단점이 있다.
이를 해결하기 위해 예비 요청을 캐싱하여 사용한다.
브라우저 캐시를 사용해서 Access-Control-Max-Age 헤더에 시간을 명시 해서 캐싱
단순 요청 (simple request)
단순 요청은 예비 요청을 생략하고 바로 서버에 본요청을 보낸 후 응답 헤더를 통해서 브라우저가 CORS 정책 위반 여부를 검사하는 방식.
대표적으로 3가지 경우를 만족 할 때만 가능하다.
- 요청의 메소드는 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중 하나여야한다. 아닐 경우 예비 요청으로 동작된다.
대부분의 HTTP API 요청은 application/json으로 통신하기 때문에 3번 조건을 만족하지 못한다.
즉, 대부분의 API 요청은 예비 요청으로 이루어진다.
인증된 요청(Crendtialed Request)
클라이언트에서 서버에 자격 인증 정보(Credential) 를 실어 요청할 때 사용되는 요청
자격 인증 정보는 세션 ID가 저장되어있는 쿠키(Cookie) 혹은 Authorization 헤더에 설정하는 토큰 값 등을 말한다.
1. 클라이언트에서 인증 정보 보내도록 설정
브라우저가 제공하는 요청 API 들은 별도의 옵션 없이는 브라우저의 쿠키 같은 인증과 관련된 데이터를 요청 데이터에 담지 않는다.
인증 정보를 담기 위해서 credentials
옵션을 사용하면 된다.
옵션 값 | 설명 |
---|---|
same-origin(기본값) | 같은 출처 간 요청에만 인증 정보를 담을 수 있다. |
include | 모든 요청에 인증 정보를 담을 수 있다. |
omit | 모든 요청에 인증 정보를 담지 않는다. |
// fetch 메서드
fetch("https://example.com:1234/users/login", {
method: "POST",
credentials: "include", // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
body: JSON.stringify({
userId: 1,
}),
})
// axios 라이브러리
axios.post('https://example.com:1234/users/login', {
profile: { username: username, password: password }
}, {
withCredentials: true // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
})
2. 서버에서 인증된 요청에 대한 헤더 설정
인증된 요청에 대해 일반적인 CORS 요청과 다르게 대응해야한다.
요구조건
- 응답 헤더의
Access-Control-Allow-Credentials
항목을 true로 설정해야 한다. - 응답 헤더의
Access-Control-Allow-Origin
의 값에 와일드카드 문자("*")는 사용할 수 없다. - 응답 헤더의
Access-Control-Allow-Methods
의 값에 와일드카드 문자("*")는 사용할 수 없다. - 응답 헤더의
Access-Control-Allow-Headers
의 값에 와일드카드 문자("*")는 사용할 수 없다.
인증된 요청도 예비 요청이 먼저 일어난다. 위의 경우 단순 GET요청이라 생략
CORS 해결 방법
Chrome 확장 프로그램 이용
브라우저의 정책을 우회해주는 크롬 확장 프로그램을 사용할 수 있다.
chrome store - Allow CORS
로컬 환경에서 API 테스트 시 CORS 문제를 해결할 수 있다.
테스트 시에 할 수 있는 임시 방편이다.
프록시 사이트 이용
모든 출처를 허용한 프록시 서버를 통하여 우회할 수 있다.
자체 프록시 서버가 아닌 프록시 사이트를 사용할 경우 api 요청이 제한되고 비싸기 때문에 궁극적인 해결책은 아닌다.
서버에서 Access-Control-Allow-Origin 헤더 세팅
직접 서버에서 HTTP 헤더 설정을 통해 허용 Origin를 명시하는 것이 가장 정석적인 해결책이다.
CORS 관련 헤더들
// Orign
Access-Control-Allow-Origin : https://naver.com
// 허용 메서드
Access-Control-Request-Methods : GET, POST, PUT, DELETE
// 요청을 허용하는 해더.
Access-Control-Allow-Headers : Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization
// 클라이언트에서 preflight 의 요청 결과를 저장할 기간을 지정
// 60초 동안 preflight 요청을 캐시하는 설정으로, 첫 요청 이후 60초 동안은 OPTIONS 메소드를 사용하는 예비 요청을 보내지 않는다.
Access-Control-Max-Age : 60
// 클라이언트 요청이 쿠키를 통해서 자격 증명을 해야 하는 경우에 true.
// 자바스크립트 요청에서 credentials가 include일 때 요청에 대한 응답을 할 수 있는지를 나타낸다.
Access-Control-Allow-Credentials : true
// 기본적으로 브라우저에게 노출이 되지 않지만, 브라우저 측에서 접근할 수 있게 허용해주는 헤더를 지정
Access-Control-Expose-Headers : Content-Length
node.js 세팅
var http = require('http');
const PORT = process.env.PORT || 3000;
var httpServer = http.createServer(function (request, response) {
// Setting up Headers
response.setHeader('Access-Control-Allow-origin', '*'); // 모든 출처(orogin)을 허용
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); // 모든 HTTP 메서드 허용
response.setHeader('Access-Control-Allow-Credentials', 'true'); // 클라이언트와 서버 간에 쿠키 주고받기 허용
// ...
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end('ok');
});
httpServer.listen(PORT, () => {
console.log('Server is running at port 3000...');
});
node express 세팅
const express = require('express')
const cors = require("cors"); // cors 설정을 편안하게 하는 패키지
const app = express();
// ...
app.use(cors({
origin: "https://naver.com", // 접근 권한을 부여하는 도메인
credentials: true, // 응답 헤더에 Access-Control-Allow-Credentials 추가
optionsSuccessStatus: 200, // 응답 상태 200으로 설정
}));
// ...
Nginx
location / {
root html;
add_header 'Access-Control-Allow-Origin' '*';
index index.html index.htm;
}
AWS S3
- S3 버킷 선택
- 권한 탭
- CORS 추가
[
{
"AllowedHeaders": [
"Authorization"
],
"AllowedMethods": [
"GET",
"HEAD"
],
"AllowedOrigins": [
"http://www.example.com"
],
"ExposeHeaders": [
"Access-Control-Allow-Origin"
]
}
]
reference
- Inpa Dev - CORS
- https://evan-moon.github.io/2020/05/21/about-cors/
- https://core-research-team.github.io/2021-04-01/Easy-to-understand-Web-security-model-story-1(SOP,-CORS)
- https://blog.devgenius.io/cors-34e947046600
- https://westsideelectronics.com/cors-and-how-to-fix/
- https://blog.bitsrc.io/4-ways-to-reduce-cors-preflight-time-in-web-apps-1f47fe7558