728x90

CORS(Cross-Origin-Resource-Sharing)

현재 내가 있는 도메인에서 다른 도메인 서버의 API를 사용해야 할 때 현재 도메인에서 다른 도메인 API 서버에 요청을 하는 것을 cross-origin 이라 하는데, 기존엔 이 방법이 보안을 이유로 막혀있었다.(SOP Same-Origin Policy) 하지만 개발자들의 필요에 의해 공식적으로 크로스 도메인 요청을 허용해야 했다. 그래서 등장한 것이 CORS.

 

즉 Cors란 Cross-Origin-Resource-Sharing의 약자로써, 서로 다른 origin 간에 통신을 하는 것이다(리소스를 공유하는 것). 그리고 이러한 통신 과정에서 발생할 수 있는 보안적인 문제로부터 클라이언트의 사용자를 보호하기 위한 보안 조치를 해두는 것이다. 

 

CORS가 작동하기 위해서 프론트엔드에서 헤더에 CORS관련 옵션을 넣어주어야 하고, 서버에선 response header에 해당 프론트엔드 옵션을 허용한다는 설정이 필요하다.

 

그렇다면 CORS가 어떻게 작동하는 지를 살펴보자. 아래의 그림을 참고하면 된다.

 

 

 

 

 

1.클라이언트에서 HTTP의 OPTIONS 메서드를 사용해서 preflight(사전요청)를 보내서 서버가 해당 요청을 응답해줄 수 있는지 먼저 확인한다. request


2.서버에서 허용한다는 응답을 한다. response


3.클라이언트에서 원래 요청하려던 request를 다시 보낸다.(필요한 HTTP 메서드(GET or POST)로) request


4.서버에서 응답한다. response

 

 

 

 

여기서 preflight란 무엇일까?

클라이언트의 요청이 서버에서 허용되는 요청인지 확인하기 위해서 원래 요청 전에 보내는 사전 요청이다. 서버에서 preflight를 받고 허용하지 않는다면 클라이언트가 원래 보내려던 요청은 보낼 수 없고, 허용한다면 클라이언트는 원래 보내려던 요청을 마침내 보낸다.

 

cors preflight를 보내지 않는 경우

cors 정책 내에서라도 preflight 없이 바로 요청을 보내는 경우도 있다. 이러한 경우를 simple request라고 한다. 

 

1. 셋중의 하나의 리퀘스트여야 한다.

  • GET
  • POST
  • HEAD

2. 자동으로 붙는 헤더여야 한다. (다음중 하나)

3. 헤더에 content-type이 다음 세 가지중 하나여야 한다.

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

 

같은 origin 인지를 구분하는 방법은 두 URL의 구성 요소 중 Scheme, Host, Port, 이 3가지 이며, 이 3가지가 동일하면 같은 origin으로 본다. 

 

그렇다면 실제로 다른 origin에서 통신을 주고 받아야 하는 경우에, cors 정책을 안전하게 통과하기 위해서는 어떤 조치를 취할 수 있을까?

1. 직접 서버 측 응답 HTTP 헤더에 Access-Control-Allow-Origin를 추가

1
2
3
4
app.get('/data', (req, res) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.send(data);
});
cs
  • 위는 간단하게, "모든 클라이언트의 요청에 대해 응답을 하겠다!" 라는 의미. 하지만 보안이 취약해지므로 실제로는 사용하지 않는 조치이다. 
  • 본인이 신뢰하는 클라이언트에 대해서만 해당 헤더를 추가해주는 것이 좋다.(실제 해당 서버에 리소스를 주고받을 클라이언트 주소들을 추가)

2. 별도의 라이브러리 사용

  • 서버 상에서 CORS 문제를 좀더 손쉽게 다루기 위해, node.js에서 이용할 수 있는 라이브러리인 cors가 존재한다.(다른라이브러이에서도 존재하고, 일반적으로 가장 많이 사용하는 방식이다.)
1
2
3
4
5
6
7
8
9
10
const express = require('express');
const cors = require('cors');
const corsOptions = {
    origin: 'http://localhost:3000'// 허락하고자 하는 요청 주소
    credentials: true// true로 하면 설정한 내용을 response 헤더에 추가 해줍니다.
};
const app = express();
 
app.use(cors(corsOptions)); // config 추가
...
cs

 

실제로 프로젝트 진행 시에 CORS 문제로 인해 same site 이슈를 겪은 적이 있다. 위에서 언급한 조치들을 다 취했는데, 가장 중요한 부분을 잊었기 때문이었다. same site 관련 설정 이슈로 인해 클라이언트 주소를 https를 통해 보안 조치를 취하였는데, 서버 주소를 https로 ssl 보안 인증을 하지 않았었다. 즉 host와 port번호가 동일하더라도, 프로토콜 또한 동일하게 해주어야 한다.
(이에 대해서는 해당 글 아래에 추가적으로 설명할 예정이다.)

서버와 클라이언트가 통신할 때도 실질적으로 클라이언트의 host/port와 서버의 host/port가 다른 경우가 꽤있다.(물론 도메인을 동일하게 설정하는 경우도 있을 가능성도 있다.) 이런 경우 실질적으로 same origin이 아닌, cross origin이므로 cors 설정 및 same site 설정에 유의해야 한다.

 

security problem

XSS(Cross-Site Scripting)

  • XSS(Cross-Site Scripting)은 웹사이트에서 의도치 않는 스크립트를 넣는 기법을 말한다.
  • 악의적인 사용자가 게시글 등에 악성 스크립트가 포함된 내용의 글을 작성하여, 이를 보게되는 다른 사용자로 하여금 원치 않는 스크립트를 실행하게 만든다.
  • 이런 방식으로 다른 유저의 쿠키 혹은 민감한 개인정보 등을 탈취하는 행위를 할 수 있다.
    -> 따라서 cookie에 민감한 정보들을 저장한 경우에 XSS 공격에 취약하다.
  • 특정 사이트에서 실행하는 스크립트를 사용자의 브라우저가 신뢰하고 있다는 점을 노리는 공격이다.

XSS 방어

  • 데이터를 입력하고, 전송하는 모든 과정에 필터링을 거침으로써 XSS를 방어할 수 있다.
  • 간단하게는, 사이트의 모든 태그 자체를 막으면 원천적으로 봉쇄할 수 있으나, 이 경우 모든 HTML 태그의 스타일링에 어려움이 생기기 때문에 실질적으론 무리가 있다.
  • 때문에, 기본적으로는 모든 태그를 막되, 사이트에 필요한 일부 태그만을 허용하는 방식으로 필터링을 거칠 수 있다.
    (이 때 정규표현식이 활용될 수 있다.)
  • 혹은 신뢰할 수 있는 기존의 XSS 방어 라이브러리를 이용하는 것도 좋은 방법이다.

 

CSRF(Cross-Site Requset Forgery)

  • CSRF(Cross-Site Requset Forgery)사이트 사용자가 자신의 의지와 무관하게 공격자가 의도하는 행위를 특정 웹에 요청하도록 만드는 공격이다.
  • 한 서버가 특정 클라이언트로부터의 오는 요청들을 신뢰한다는 부분을 노리는 공격.
  • XSS와 연계되는 경우도 많은데, XSS 공격을 통해 유저의 쿠키 및 개인정보를 탈취한 후, 이로부터 관리자 권한을 얻어서 서버에 부적절한 요청을 보내기도 한다.
  • 해커는 희생자의 권한을 도용하여 중요 기능을 실행하는 것(그러나 직접 데이터에 접근할 수 있는 것은 아니다. request를 조작하여서, 요청자인 척, 데이터를 받아볼 수 있도록 하는 것이다.)

[이미지 출처 :  http://www.bluekaizen.org] 

CSRF 공격을 받기 쉬운 조건

  • 쿠키를 사용한 로그인
  • 예측하기 쉬운 요청/parameter

CSRF 예시

페이스북에 글을 쓸 때 아래 코드와 같은 폼이 전송된다고 예를 듭시다. 피싱 사이트에 똑같이 페이스북에 글쓰기를 요청하는 폼이 숨겨져 있고, 그 내용으로 가입하면 10만원을 준다는 사기성 광고를 본문으로 적혀져 있습니다. 희생자는 피싱 사이트에 접속함으로써 본인의 페이스북 계정으로 해당 글이 등록되게 됩니다.

CSRF 방어

일반적으로 CSRF 공격 방어는  GET Method에는 방어 대상에 두지 않고, 쓰기/변경이 가능한 POST, PATCH, DELETE Method에만 적용하면 된다. 물론 정말 중요한 데이터를 조회하거나 GET을 통해 쓰기/변경 등의 동작을 한다면 GET Method에도 방어를 해야한다.

Referer 체크

  • HTTP 헤더에 있는 referer 정보를 체크해, 해당 요청이 신뢰가능한 페이지로부터 온 것인지 확인한다. 단, HTTP의 헤더 정보는 조작이 가능하기에, 완전한 방어가 될 수 없다.
    ->  같은 도메인 내의 페이지에 XSS 취약점이 있는 경우 CSRF 공격에 취약해질 수 있기 때문이다.

토큰 발급

  • 로그인한 유저에 대해 매번 새로운 토큰을 발급함으로써, 서버에 요청을 보낼 때는 항상 해당 토큰을 함께 전송해야만 이에 대한 응답을 하는 방법. (이 또한 XSS에 취약점이 있다면 공격에 취약할 수 있다.)
  • 사용자의 세션에 임의의 난수 값을 저장하고 사용자의 요청 마다 해당 난수 값을 포함 시켜 전송시킵니다. 이후 Back-end 단에서 요청을 받을 때마다 세션에 저장된 토큰 값과 요청 파라미터에 전달되는 토큰 값이 일치하는 지 검증
1
2
3
4
5
// 로그인시, 또는 작업화면 요청시 CSRF 토큰을 생성하여 세션에 저장한다. 
session.setAttribute("CSRF_TOKEN",UUID.randomUUID().toString()); 
 
// 요청 페이지에 CSRF 토큰을 셋팅하여 전송한다 
<input type="hidden" name="_csrf" value="${CSRF_TOKEN}" />
cs

 

Double Submit Cookie 검증

  • 세션을 사용할 수 없는 환경에서 사용할 수 있는 방법이다.
  • 웹브라우저의 Same Origin 정책으로 인해 자바스크립트에서 타 도메인의 쿠키 값을 확인/수정하지 못한다는 것을 이용
  • 스크립트 단에서 요청 시 난수 값을 생성하여 쿠키에 저장하고 동일한 난수 값을 요청 파라미터(혹은 헤더)에도 저장하여 서버로 전송

 CAPTCHA 사용

  •  캡차이미지상의 숫자/문자가 아니라면 즉, 적절한 인증코드가 담긴 요청인 경우, 해당 요청을 거부하는 것이다. 그러나 이 방식에 CSRF를 막을 수 있는 최선의 방법은 아니다.
  • (링크 참고)

쿠키옵션을 설정할 때 SameSite 설정으로도 CSRF 공격을 방지할 수 있다. 

위의 경우에 대비한 SameSite 설정

 

 

 

Reference

 

참고 블로그(1)

참고 블로그(2)

CSRF 참고 블로그(1)

CSRF 참고 블로그(2)

 

 

728x90

+ Recent posts