CORS에 대해서 다루려면, 우선적으로 SOP에 대해서 먼저 이해를 해야 한다.
SOP는 Same-Origin-Policy의 약자로, 브라우저에 로드되어 있는 document나 script가 다른 origin의 리소스와 상호작용을 하는 것을 막는 정책이다.
이것이 가능한 이유는 크롬을 기준으로 설명하자면 브라우저가 생성하는 탭은 각각의 탭마다 제각각의 프로세스에서 실행이 되고 있기 때문이다. 즉, 하나의 origin을 기준으로 로드된 document의 내부에 다른 오리진에 대한 로드 정보가 존재할 경우, 이 내용은 그 프로세스 내에서 실행되는 것이 아니라 다른 프로세스에서 자원을 할당받아 새롭게 실행되고 있고 서로 다른 프로세스끼리는 상호 접근이 제한되어 있기 때문에 Same Origin Policy를 실현시키기 용이하다.
그렇다면 여기서 origin의 기준을 알아야 한다.
개발자 탭을 열어서 이미 로드된 document에서 origin을 검색해보면 다음과 같은 결과가 나온다
해당 내용에는 한가지가 빠져있는데 그것은 port 번호이다.
다시말해 origin이 같다는 기준은 아래와 같다.
동일한 protocol을 사용해야 한다.
여기서 프로토콜이란, 컴퓨터가 서로 상호간에 데이터를 주고 받기 위해 설정하는 통신 규약이다. 데이터의 타입에 따라 네트워크의 연결방식이나 코드 변환 방식이 서로 다르기 때문에 프로토콜을 지정해 둠으로서 사전에 어떠한 형태로 데이터를 주고받을 지를 정의하여 불필요한 통신 오류를 줄일 수 있다. 프로토콜은 예를 들면 http, FTP, SMTP와 같은 종류가 있다.
동일한 host여야 한다.
호스트란 인터넷과 연결되어 있는 컴퓨터(호스트) 를 뜻한다. 보통 해당 호스트에는 도메인 네임이 붙어있다.
도메인 네임은 컴퓨터에 부여되어 있는 ip 주소를 사람이 읽을 수 있는 형태로 변환한 네이밍이다. 해당 key-value 연결을 DNS 라고 하는 서비스가 해주고 있고, 사용자가 도메인 네임으로 url을 주소창에서 입력했을 경우 브라우저 캐싱부터 시작하는 로컬 DNS에서 해당 내용을 찾아보고 없다면 루트 DNS로 이동하여 top level domain (ex .com) 을 담당하는 서버부터 시작하여 연쇄적으로 해당 연결고리를 찾아내는 Recursive Query를 진행한다
동일 포트여야 한다.
포트란 해당 프로그램이 돌아가는 프로세스의 번호이다. 컴퓨터 내부의 프로세스는 모두 제각각 자신이 실행되는 환경인 포트번호가 존재하고, 이 포트번호를 통해 네트워크 요청이 들어오거나 나오는 진입로 역할을 하게 된다.
만약 SOP가 적용되어 있지 않다면, 위와 같은 케이스가 발생할 수 있다.
1. 한 document가 로드되어 있는 상태에서 내부에 해커가 심어놓은 URL을 클릭한다
브라우저는 해당 URL로 요청을 보내서 document를 받고 새로운 프로세스에서 이것을 실행한다.
내부에는 iframe이 존재하므로 새로운 프로세스가 열려 구글의 mail로 get 요청을 날린다. 이때 iframe의 오리진은 요청 대상 서버의 오리진과 서로 같으므로(same-origin) 자동으로 cookie가 함께 전달되고, 구글 사이트는 이 쿠키를 보고 사용자를 인증하여 메일정보를 보낸다.
해커의 script 태그에 존재하는 addEventListener은 로드가 되는 순간 함수를 실행시켜 해당 iframe 태그가 로드해온 구글 메일 정보를 해커에게 전송하여 탈취를 성공시킨다. (SOP 가 존재하지 않는다면 iframe에서 관리되는 google 오리진의 프로세스 자원을 suspicious.com에서 만들어진 프로세스가 접근하여 가져올 수 있게 된다)
위의 경우 쿠키는 same site 옵션이 적용되지 않는 상황까지 가정된 것이다.
이렇게 특정 url을 누르자마자 모든 정보가 탈취가 되는 상황이 벌어질 수 있다.
즉, 다시 말하자면 서로 다른 오리진을 기반으로 하는 프로세스끼리의 자원이동에 제약이 전혀 존재하지 않는다는 뜻이다.
하지만 여기서 SOP 가 적용된다면 이야기는 달라진다.
SOP가 적용되었을 경우, 마지막 시나리오에서 www.suspicious.com이라는 오리진을 가진 프로세스에서 https://mail.google.com/mail/inbox 의 오리진을 가진 프로세스가 관리하고 있는 자원을 가져오지 못하게 된다!
관리 자원을 가져오지 못한다는 뜻은 위에서 본다면 document.getElementById("mail").contentDocument.body 를 참조하지 못한다는 뜻이다. (Iframe은 완전히 별개의 프로세스에서 관리되는 새로운 탭이라고 생각하면 좋다. 이 내용은 null 이 나오게 된다.)
AJAX 역시 비슷하게 다른 Origin일 경우 접근을 차단하게 된다.
기술개발이 되면서 꼭 브라우저의 url 창에 주소를 입력하지 않더라도 함수를 통해 서버로부터 리소스를 받아와 저장하는 방식이 개발되었고 그것을 AJAX라고 칭하는데, 이때에는 suspicious.com의 document 내부에서 해당 함수가 호출이 되면서 데이터를 받아오려고 시도할 것이다.
즉, iframe의 경우 아예 새로운 프로세스가 생성되면서 거기에서 받아온 데이터를 www.suspicious.com 오리진의 프로세스가 접근하려고 했던 것이고, AJAX는 www.suspicious.com이 로드된 오리진의 프로세스 내에서 google 서버로 요청을 날리는 상황과 같다.
위와 같은 경우, 네트워크를 통해 받아온 데이터에 존재하는 origin 정보와 AJAX 요청을 시도하였던 document의 오리진이 다르기 때문에, 즉 해당 프로세스로 네트워크 데이터가 전달되려고 했으나 오리진이 다르기 때문에 전달 자체가 되지 않게 되는 것이다.
결국 AJAX요청을 실행한 xhr객체 내부에는 아무런 결과값이 들어오지 않게 된다.
note. 중요한 것은 해당 데이터의 파기 여부를 실행되고 있는 브라우저의 내부 로직에서 관리한다는 점이다. 즉, cross origin으로 오는 데이터의 경우 랜더링 프로세스에서 해당 오리진이 다르다고 결론짓고 해당 response를 파기한다. 반대로 말하자면 서버간의 통신일 경우는, 즉 node.js 서버에서 타 서버에 요청을 날리는 경우라면 origin이 다르더라도 response를 파기하는 행위를 하지 않기 때문에 받아들일 수 있는 것이다. 다시 말해! 서버간의 통신에서는 CORS가 적용되지 않는다.
그러나 현실적으로 웹개발을 하다 보면 서로 다른 origin끼리의 리소스를 가져와야 할 필요가 존재하게 된다.
즉 예를들어, axios를 통한 AJAX 요청으로 다른 서버에 존재하고 있는 데이터를 로드해 우리의 웹사이트에 반영해야 한다고 가정하자. 이때 SOP가 일반적으로 적용되어 있기 때문에 해당 Axios 객체의 get 메소드를 실행한 document 프로세스의 origin은 자원을 요청한 타 서버의 origin과 다르기 때문에 네트워크를 통해 건너온 데이터가 해당 프로세스로 진입할 수가 없게 된다.
하지만 이때, 서버 측에서 전달하는 response 헤더로 해당 origin의 프로세스는 접근할 수 있도록 허용한다는 의미의 "Access-Control-Allow-Origin" 내부 배열 정보에 해당 오리진이 정의되어 있다면 프로세스로 진입할 수 있게 해줄 수 있다.
즉, CORS는 Cross-origin-resource-sharing의 약자로, 위에서 설명했듯 다른 오리진으로 실행되고 있는 프로세스에 접근할 수 있는 허용조건을 명시하는 규약인 것이다. (차단 규약이 아닌 것!)
Node.js 서버 기준으로 해당 허용 오리진들을 배열 형태로 설정해줄 수 있다.
여기서 보이는 "credentials"은 쿠키에 대한 이동방식의 내용이다.
기본적으로 쿠키는 SOP일 경우라면 자동으로 붙어서 전달이 된다. 즉, 같은 오리진을 지닌 프로세스끼리는 서로간에 쿠키가 자유롭게 전달이 되고, 또 내부 로컬의 프로세스가 아닌 외부 서버의 컴퓨터에서 구동되는 프로세스라 하더라도 마찬가지로 동일하게 http 헤더에 쿠키가 붙어서 전달되는 것이 가능하다.
그런데 만약 오리진이 서로 다를 경우라면 쿠키가 전달이 되지를 않는다.
따라서, 서로 다른 오리진끼리 쿠키를 주고받는것을 명시적으로 서로 허용해줘야 한다.
브라우저는 ajax를 보내는 객체의 메서드에서 옵션으로 (aixos의 경우) withCredential:true를 설정해줘야 하고, 서버는 들어오는 요청들에 대해 origin을 설정하면서 "credential:true"를 설정해주거나 응답 헤더에 "Access-Control-Allow-Credentials : true "를 넣어줘야 한다.
여기에 덧붙여서, same-site 옵션을 통해 설령 동일 사이트라 하더라도 쿠키의 전송을 제약하는 것이 가능하다. 예를들어
위의 케이스를 살펴보자면, form 태그를 통해 해당 외부 서버에 요청을 날리게 된다. 이때, 요청 자체의 url과 타겟이 되는 url이 동일하므로 랜더링 프로세스는 브라우저의 네트워크 쓰레드를 이용하여 쿠키와 함께 요청을 날려버린다.
즉, 위와 같이 쿠키를 날려서 post로 기존 유저정보를 바꿔버리는 경우 문제가 발생하는데 이것이 바로 CSRF 공격이다. 다시말하면 브라우저가 단순하게 url이 같다는 이유만으로 쿠키를 같이 전송해서 생기는 문제라는 뜻이다.
이것을 막기 위해, cookie의 same site 옵션을 적용해줄 경우 (ex strict)
내가 접속한 사이트의 문맥상 url (즉 브라우저에서 보이는 url 주소창의 url) 와, 전송 대상이 되는 url이 서로 다를 경우라면 쿠키역시 전송되지 않는 옵션을 설정해줄 수 있다.
요청에 대해서 서버에 침습적인 요청일 경우 (ex, 메서드가 post거나 delete처럼) 이것은 simple request가 아니라고 판단하여 우선적으로 서버에게 해당 요청을 보내는 것이 가능한지를 확인하는 preflight요청을 option 메서드를 이용하여 보내게 된다. 서버는 해당 메서드를 확인한 후 자신의 내부 옵션에서 해당 요청에 대해 응답할 수 있다고 설정이 되어있는지를 확인해보고 맞다면 헤더에 "Allow-Control-Allow-Origin" 즉, 해당 오리진에서 오는 요청에 대해서 서버 내부의 control을 허용한다는 헤더를 붙여서 전송한다.
만약, 이 헤더가 붙지 않는다면 다시금 브라우저는 CORS 에러를 내게 된다.
이런 preflight를 통해 요청에 대해서 미리 사전검증을 함으로써 서버의 부담을 경감함과 동시에 기존 SOP에 대한 정책만 가지고 있던 서버들이 cross-origin으로부터 오는 악의적인 요청에 대해 대응할 수 있도록 대비한 방지책이라고 할 수 있다.