가장 처음 개발을 했을 때가 생각이 난다. API를 만들었었는데 안된다고 CORS 풀어줬냐는 다른 팀원의 질문에 나는 아무 대답도 하지 못했었다. 지금은 자신있게 풀어놨다고 할 수 있지만 아직도 정확한 정의에 대해서는 헷갈려한다. 따라서 이번에 CORS에 대해서 한번 알아보고자 한다!
CORS에 대해서 이해하기 위해선 SOP라는 것을 먼저 이해해야한다. SOP에 대해서 살펴본 뒤에 CORS에 대해서 알아보자!
Same-Origin Policy의 약자로 직역하면 동일 출처 정책이다. 이 SOP는 웹 브라우저 보안의 핵심 원칙 중 하나로, 1990년대 후반에 Netscape Navigator 브라우저에서 처음 도입되었다.
SOP는 웹 페이지의 스크립트가 자신이 속한 출처와 동일한 출처에 있는 리소스에만 접근할 수 있도록 제한하는 보안 메커니즘이다. 여기서 출처는 orgin이라고도 하며, 도메인(domain), 프로토콜(protocol), 그리고 포트(port)를 포함하여 정의된다.
예를 들어, URL이 https://www.example.com:8080/path 라고 한다면, 프로토콜은 https , 도메인은 www.example.com, 포트는 8080 가 된다.
즉, URL의 출처는 https://www.example.com:8080이 되는 것이다!
SOP의 주된 목적은 사용자가 다른 웹 사이트를 방문할 때 그 사이트의 스크립트가 사용자의 데이터를 무단으로 접근하거나 조작하지 못하도록 하는 것이다. 이로써 악성 웹 사이트가 사용자의 다른 사이트 세션, 쿠키 또는 민감한 데이터를 탈취하는 것을 방지한다.
예를 들어, 사용자가 https://angel.com에 로그인한 상태에서 악성 웹 사이트(https://devil.com)를 방문했을 때, https://devil.com의 스크립트가 SOP에 의해 https://angel.com의 데이터에 접근할 수 없다.
이처럼 악성 웹 사이트의 접근을 막아주는 역할을 하기 때문에 SOP를 중요한 보안 메커니즘이라고 한다!
위에서 봤던 것처럼 이 SOP는 강력한 보안 장치이지만, 웹 애플리케이션이 더 복잡해지고 API 기반 통신이 증가함에 따라 유연성의 부족이 문제로 나타났다.
유연성이 부족할 일이 뭐가 있어? 라고 할 수 있는데, 다음과 같은 상황을 살펴보면 이해가 될 것이다.
다른 도메인에 있는 API 호출
: 웹 애플리케이션이 외부 API(다른 도메인에 위치한 리소스)와 통신해야 하는 경우, SOP가 이를 차단하여 통신이 불가능
CDN을 통한 자원 로드
: 웹 페이지가 여러 출처에서 리소스를 로드할 때, SOP는 이를 제한하여 페이지 로딩이나 기능 구현이 어려움
💡 CDN?
Content Delivery Network의 약자로 지리적으로 분산된 서버들을 연결한 네트워크
어렵게 얘기했지만 결국 같은 얘기다. 내 서버에서만 동작을 하는 것이 아니라 다른 서버와 통신을 해야하는데 SOP가 이를 막아버리면 그 이상의 기능 구현이 어려울 수 있다.
때문에 이러한 SOP의 한계를 극복하기 위해 CORS가 등장하게 되었다!
Cross Origin Resource Sharing의 약자로, 직역하면 교차 출처 리소스 공유 정책이다.
주로 CORS Error라고 불리는 이것은 웹 개발 신입 신고식이라고 할 정도로 누구나 한 번쯤은 겪는 에러이다. 그런데 이상한 것이 있다. CORS의 정의에서 우리는 오류가 아닌 정책이라고 했다. 왜 에러가 아니라 정책이라고 부르는 것일까?
위에서도 얘기했지만 사실 CORS는 다른 출처의 리소스 공유에 대해 허용하거나 비허용하는 정책이다. 개발을 하다보면 기능상 다른 출처 간의 상호작용이 필요한 경우가 발생하는데 이때, 예외사항을 두기 위해서 CORS 정책을 허용하는 리소스에 한해 다른 출처라도 받아들이는 것이다!
때문에 사실 CORS는 사실 해결책이라고 할 수 있다. 실제로 Error Message를 살펴보면 브라우저의 SOP 정책에 따라 다른 출처의 리소스를 차단하면서 발생된 에러라고 한다. 따라서 이 다른 리소스를 허용해주기 위해서 CORS 정책을 따르게 해주는 것이다.
즉, 우리는 CORS 정책을 따르게 해서 SOP 정책을 회피하는 동작을 구현해야하는 것이다!
CORS에는 주로 다음과 같은 3가지 정책이 적용된다.
Simple Requests (단순 요청)
단순 요청은 특정 조건을 충족하는 GET, POST, HEAD 메서드의 HTTP 요청이다. 이러한 요청은 사전 검사(Preflight)를 필요로 하지 않으며, 브라우저는 요청과 함께 Origin 헤더를 서버로 전송한다. 서버가 응답 헤더로 Access-Control-Allow-Origin을 포함시키면, 브라우저는 요청을 허용한다.
단순 요청의 조건은 다음과 같다.
application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나일 것Preflight Requests (사전 요청)
사전 요청은 특정 조건을 만족하지 않는 요청이 발생할 때, 실제 요청을 보내기 전에 브라우저가 서버의 허용 여부를 확인하기 위해 보내는 OPTIONS 메서드의 HTTP 요청이다. 브라우저는 이 요청을 통해 서버가 실제 요청을 허용하는지 확인한 후, 허용된다면 실제 요청을 수행한다.
Preflight 요청의 조건은 다음과 같다.
Content-Type이 단순 요청 조건에 맞지 않는 경우또한, Preflight 요청은 서버의 응답이 아래 헤더를 포함해야 허용이 된다.
Credentialed Requests (자격 증명 요청)
자격 증명 요청은 인증된 사용자 데이터(예: 쿠키, HTTP 인증 정보)를 포함한 요청이다. 이 경우, 서버는 응답 헤더에 Access-Control-Allow-Credentials: true를 설정해야한다. 브라우저는 withCredentials 옵션이 설정된 요청에 대해 이 헤더가 없는 응답을 거부한다.
자격 증명 요청의 주요 고려사항은 다음과 같다.
*(와일드카드)가 아닌 특정 출처를 명시이렇게 CORS에 대해서 알아보았다. 그렇다면 이번엔 CORS로 해결하는 방법에 대해서 알아보자. 그냥 코드로 구현하면 되는 거 아니야? 라고 할수도 있는데, 생각보다 다양한 방법으로 우회할 수 있다!
개발자들은 브라우저의 확장 프로그램을 사용하여 CORS 문제를 일시적으로 우회하고, 로컬 환경에서 API를 쉽게 테스트하기 위해서 사용한다. 이러한 확장 프로그램을 사용하면 CORS 요청을 허용하도록 설정을 쉽게 변경할 수 있다!
다만 이 방법은 개발 및 테스트 환경에서는 유용할 수 있으나 실제 환경에서는 보안 위험이 크기 때문에 사용을 하지 않는 것이 좋다.
클라이언트가 직접 서버에 리소스를 요청했는데 서버에서 따로 설정을 해주지 않아 CORS 에러가 뜰 경우, 직접 보내지 말고 중간에 위치한 프록시 서버를 통해서 요청을 보내면 된다!
프록시 서버는 클라이언트와 동일한 출처에서 동작하며, 클라이언트의 요청을 받아 API 서버로 전달한 뒤 응답을 다시 클라이언트로 보내준다. 이 방법을 사용하면 CORS 정책을 우회할 수 있다.
다만, 현재 무료 프록시 서버 대여 서비스는 사용에 무리가 있다. 그 이유는 악용 사례가 너무 많기 때문에 API 요청 횟수에 제한을 두었기 때문이다. 따라서 테스트를 하기 위해서 직접 프록시 서버를 구축해서 사용해야할 수도 있다.
프록시 서버를 구축해놓은 곳들이다.
Request temporary access to the demo server를 클릭하여 데모 서버 활성화
다만 시간 제한이 있어 임시방편의 해결방법
사용 코드
const url = 'https://google.com' // 이 부분을 이용하는 서버 URL로 변경
fetch(`https://cors-anywhere.herokuapp.com/${url}`)
.then((response) => response.text())
.then((data) => console.log(data));
<script src='https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js'></script>
<script>
axios({
url: 'https://cors-proxy.org/api/',
method: 'get',
headers: {
'cors-proxy-url' : 'https://google.com/' // 이 부분을 이용하는 서버 URL로 변경
},
}).then((res) => {
console.log(res.data);
})
</script>실전에서 이용하려면 유료 결제가 필요(free도 가능함)
사용 코드
const url = 'https://google.com' // 이 부분을 이용하는 서버 URL로 변경
fetch(`https://proxy.cors.sh/${url}`)
.then((response) => response.text())
.then((data) => console.log(data));
가장 일반적이고 권장되는 방법이다. 서버 측에서 CORS 설정을 올바르게 구성하는 것인데, 서버가 허용할지 말지의 여부를 결정하고 허용할 경우 HTTP 헤더를 설정하여 응답을 하게 된다.
이 방식은 보안이 가장 잘 보장이 되는 방식이며, 클라이언트와 서버 간의 신뢰 관계를 설정할 수 있다!
설정하는 방법은 다음과 같다!
각 서버의 문법에 맞게 HTTP 헤더를 추가
HTTP 헤더
# 헤더에 작성된 출처만 브라우저가 리소스를 접근할 수 있도록 허용함.
# * 이면 모든 곳에 공개되어 있음을 의미한다.
Access-Control-Allow-Origin : https://naver.com
# 리소스 접근을 허용하는 HTTP 메서드를 지정해 주는 헤더
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
서버별 세팅 방법
<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...');
});
```
// 스프링 서버 전역적으로 CORS 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080", "http://localhost:8081") // 허용할 출처
.allowedMethods("GET", "POST") // 허용할 HTTP method
.allowCredentials(true) // 쿠키 인증 요청 허용
.maxAge(3000) // 원하는 시간만큼 pre-flight 리퀘스트를 캐싱
}
}
``` // 특정 컨트롤러에만 CORS 적용하고 싶을때.
@CrossOrigin(origins = "*", methods = RequestMethod.GET)
@Controller
public class customController {
// 특정 메소드에만 CORS 적용 가능
@GetMapping("/url")
@CrossOrigin(origins = "*", methods = RequestMethod.GET)
@ResponseBody
public List<Object> findAll(){
return service.getAll();
}
}
```
location / {
root html;
add_header 'Access-Control-Allow-Origin' '*';
index index.html index.htm;
}[
{
"AllowedHeaders": [
"Authorization"
],
"AllowedMethods": [
"GET",
"HEAD"
],
"AllowedOrigins": [
"http://www.example.com"
],
"ExposeHeaders": [
"Access-Control-Allow-Origin"
]
}
]실무에서 자주 발생하는 상황은 아니지만 결국 CORS의 문제가 또 발생하기도 한다. 다만 심화적인 내용이라 난이도가 꽤 있기 때문에 지금 자세히 다루지는 않겠다.. 다음에 기회가 된다면 그때.. 조금 더 깊게…
SOP 정책으로 막아둔 부분을 CORS 정책으로 억지로 뚫었기 때문에 결국 보안의 취약점이 존재할 수밖에 없다. 특히 위에서 계속 강조했던 것 처럼 허용할 도메인 설정을 * (와일드카드)로 해두는 경우가 있다.
이때 이 요청의 흐름을 악용하여 타인의 개인 정보 해킹의 위험성이 존재한다.
PNA란 Private Network Access로 사설망 접근이다. 이 사설망 접근이 왜 나왔을까?
CORS는 다른 네트워크의 리소스를 호출하기 위해서 사용한다고 했다. 그렇다면 공인된 정부나 군사 시설의 네트워크의 웹 사이트에서 사설 네트워크의 리소스를 호출하려고 할 때 어떻게 될까?
맞다. 공인 네트워크가 아닌 사설망의 접근이기 때문에 조금 더 조심스럽다. 따라서 이때 나오는 CORS 에러는 일반적인 CORS 에러와 조금 다른 Message의 형태를 띈다고 한다. 이를 해결하기 위해서는 사설망 접근에 대한 지식도 알고 있어야 한다!
브라우저는 이미지와 같은 리소스에 대해서 요청을 받으면 자동으로 Cache 작업을 진행한다. 이미지와 같은 리소스는 한번 응답을 받을 때마다 시간이 걸리기 때문에 나중에 같은 리소스를 재요청할 때 Cache 저장소에서 가져와 성능상으로 이점이 존재하기 때문이다.
그러나 Cache는 최신 리소스 불일치 현상 등과 같은 자잘한 문제점이 존재하게 되는데, 이러한 Cache 때문에 갑자기 CORS 에러가 발생할 수 있다!
이처럼 CORS 정책을 해결하기 위해선 단순 풀어주는 작업 뿐만 아니라 조금 더 심도 깊은 내용들이 필요할 수 있다…! 공부 열심히 하자..!
SOP는 동일한 출처에서만 요청이 가능한 정책이다!
CORS는 SOP로 인한 제한을 풀어주기 위한 정책으로, 다른 출처 사이의 리소스 공유가 가능하도록 한다!