CSRF와 XSS 개념에 대해 정리하자 ✌🏻
1. CSRF
- CSRF(Cross-Site Request Forgery)란 사이트 사이의 요청 위조이다.
- 웹 어플리케이션의 취약점 중 하나로, 인터넷 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 특정 웹사이트에 요청하도록 만드는 공격이다.
- 공격자는 웹사이트가 신뢰하고 있는 사용자의 권한을 이용해 서버에 변조된 요청을 보내 공격한다.
공격 동작
- 공격자는 이메일이나 게시판에 CSRF 스크립트가 포함된 게시물을 업로드한다.
- 사용자는 공격자가 등록한 게시물을 열람한다.
(img
태그 width, height가 0px으로 되어있거나,form
내부input
태그가 hidden으로 설정되어있다면 알아차리지 못할 수 있다. 혹은form
자동 submit 스립트를 실행시킬 수 있다) - CSRF 스크립트가 실행되어, 공격자가 의도한 공격이 사용자 권한으로 실행된다. 💣
사용자가 게시물을 열람했을 때, 공격자가 로그아웃을 요청하거나 다른 공격을 취할 수 있다. 이 공격은 사용자 권한으로 실행되어 정상적으로 실행되고 만다. 만약 사이트 관리자의 계정이 공격당했을 경우 관리자의 비밀번호를 변경해서 공격자가 관리자 계정에 접근이 가능해 질 수 있다.
CSRF 공격 해결 방법
1. Referer 체크
- 서버에서 사용자의 Referer를 체크하는 방법. Referer는 HTTP header에 들어있다.
- 내가 어디 경로를 통해 지금 사이트로 왔는지 알 수 있다. (URI를 표시)
- 허용하려는 호스트를 등록시킨 후, 사용자의 요청 헤더 referer값이 등록된 값과 동일하다면 허용한다.
- 허용되지 않은 호스트에서 오는 요청은 막을 수 있다.
구글에서 네이버를 검색해서 들어간 후 document.referrer
을 입력해보면 아래와 같다. 어떤 페이지에서 네이버로 들어왔는지 알 수 있는 것이다.
2. Token 검증
- 서버에 들어온 요청이 허용한 요청이 맞는지 확인하기 위한 토큰을 사용한다.
- 사용자가 서버에게 요청을 필요로하는 페이지를 열었을 때, 임의의 난수로 생성된 Token을 발급해서 클라이언트에게 전달한다. 그 후 사용자가 실제 요청을 보냈을 때, 해당 토큰을 같이 전달한다. 서버는 자신이 발급해준 Token이 맞는지 검사한 뒤 사용자 요청을 처리한다.
3. 추가인증수단(ex. CAPCHA)
- "로봇이 아닙니까?" 물어보거나 신호등그림 찾는것처럼, 추가 인증 수단을 거쳐 통과하지 못한다면 요청 자체를 거부하는 방법
2. XSS
- 크로스 사이트 스크립팅(Cross Site Scripting, XSS)은 공격자가 사용자 브라우저에 악성 스크립트를 주입시켜 사용자의 세션을 가로채거나, 웹사이트를 변조하는 등의 공격이다.
- CSRF가 웹 사이트가 사용자를 신뢰한다는 점을 공격하는 거라면, XSS가 사용자가 특정 사이트를 신뢰한다는 점을 공격하는 것이다.
- CSRF는 악성 코드가 서버에서 발생하고, XSS는 악성코드가 클라이언트에서 발생생한다고 볼 수 있다.
XSS 종류
- Persistent(or Stored) XSS
- 지속적으로 피해를 입히는 공격
- 공격자가 웹사이트에 악성 스크립트를 삽입하는 방식으로 진행
- 공격 스크립트가 DB에 저장이 되고, 악성 스크립트가 존재하는 게시글 등을 열람한 사용자들이 공격 대상이 됨
- DB에 저장되어 지속적으로 공격하기 때문에 많은 피해가 발생할 수 있음
- Reflected XSS
- 사용자에게 입력 받은 값을 서버에서 되돌려주는 곳에서 발생
- 사용자가 직접 스크립트를 실행하도록 유도하는 공격
- 사용자가 버튼 또는 링크를 클릭함으로써 악성 스크립트가 실행된다.
- DOM based XSS
- 브라우저를 해석하는 단계에 발생하는 공격
- 악의적인 스크립트로 인해 클라이언트 측 코드가 원래 의도와는 다르게 실행
- 서버 측에서 탐지하기 어렵다.
해결방법
- 입력값에서
<
,>
등을 사용해 태그를 조작할 수 없도록 한다. 예를 들어,<
,>
를<
,>
같은 문자열로 치환시키는 방법이 있다. React에서는 입력된 값을 렌더링하기 전에 문자열로 변환한다. 하지만 이런 방식 외에도 XSS 공격을 가할 수 있는 방법은 다양하기 때문에 입력값을 제한하거나 대응방안을 계속해서 생각해야 한다.
3. CSRF 방어 설계
나는 CSRF 공격을 방어하기 위해 Referer체크와 CSRF 방어 토큰을 사용했다. 어떻게 구현했고 왜 이런 방법을 선택했으며, 구현 시 주의할 점도 함께 정리하려고 한다.
1. Referer 체크
try {
// referer값 추출
String referer = request.getHeader("Referer") != null ? request.getHeader("Referer") : null;
/**
* https://test.com/api/ => https://test.com
* http://test/api/ => http://test
* http://www.test/api => http://www.test
* http://localhost:3000/api => http://localhost:3000
*/
String regex = "https?:\\/\\/(?:localhost|(?:w{1,3}\\.)?[^\\s.]*(?:\\.[a-z]+))*(?::\\d+)?(?![^<]*(?:<\\/\\w+>|\\/?>))";
Pattern refererPattern = Pattern.compile(regex);
Matcher match = refererPattern.matcher(referer);
if (!match.matches()) {
// 올바른 url 패턴이 아닌경우
throw new RefererAccessDeniedException("Referer URL pattern not matched.");
} else {
// referer white list에 없는 도메인인 경우
if (!refererWhiteList.contains(match.group())) {
throw new RefererAccessDeniedException("Referer access denied.");
}
}
} catch (Exception e) {
throw new RefererAccessDeniedException("Referer not allowed.");
}
- 현재 들어온 요청 헤더의 referer 값에서 Java 정규표현식(Regex)을 통해 내가 원하는 부분만 URL에서 찾아 추출한다.
- 이 값이 referer 허용 리스트(`refererWhiteList`)에 포함된다면 요청을 허용
- 만약 referer값이 다르다면 예외(`RefererAccessDeniedException`)가 터지도록 했다. (직접 커스텀한 예외이다)
Pattern 클래스
- `compile(String regex)` : 주어진 정규표현식으로부터 패턴을 만든다.
- `matcher(CharSequence input)` : 패턴에 매칭할 문자열을 입력해 Matcher를 생성한다.
Matcher 클래스
- `matches()` : 대상 문자열과 패턴이 일치하는 경우 true를 반환, 그 위치로 이동한다.
- `group()` : 매칭된 부분을 반환한다.
=> ^referer값을 체크한다면 공격사이트에서 내 서버에 동작을 요청하더라도 referer 허용 리스트(`refererWhiteList`)에 존재하지 않으므로 요청을 제한시킬 수 있다.^
위에서 사용한 정규표현식은 regexr.com 여기서 테스트가 가능하다. 파란색으로 하이라이트 되어있는 부분이 추출되는 것이다.

2. CSRF 방어 토큰
먼저 CSRF 토큰을 생성하는 API와, 실제 요청을 보내는 API를 분리해야 한다.
로직 흐름
- 클라이언트에서 GET을 제외한 요청(POST, PATCH, PUT 등)을 보내기 전에 CSRF 토큰 생성 api(GET 요청)를 먼저 요청한다.
- 서버는 CSRF 토큰 2개(A, B)를 만든다.
- 토큰A는 임의의 난수를 담고, 토큰B는 토큰A의 난수값을 JWT로 만든다.
(두 토큰 모두 쿠키로 전달하는데, 접근 가능한 도메인을 설정한다. 또, 사이트 외부에서의 쿠키에 대한 접근을 막는다. 쿠키B는 HttpOnly설정을 걸어 브라우저에서 쿠키를 조작을 할 수 없도록 한다. 여기서 쿠키A와 쿠키B의 유효시간을 아주 짧게 잡아둔다) - 클라이언트로 두 개의 토큰 모두 전달한다. 클라이언트는 응답을 받은 후 바로 실제 요청(POST, PATCH, PUT 등)을 실시하는데, 쿠키A의 값을 request header에 특정 값에 넣어 전달한다. (HttpOnly로 설정된 쿠키B도 함께 전달된다)
- 서버에서는 쿠키B의 담겨진 값과 header에 넘어온 값을 비교하여, 서버에서 발급한 토큰이 맞는지 검사하고 그에 따라 요청을 실행한다.
보통 CSRF 토큰 관련 로직을 검색해보니, 사용자가 서버에게 요청을 필요로하는 페이지를 열었을 때 토큰을 발급해서 클라이언트에게 전달하고, 그 후 실제 요청을 보낼 때 해당 토큰을 함께 전달하고 있었다.
페이지에서 요청을 보내지 않고 나가는 경우, 페이지 접근했을 때 토큰을 요청하는 것은 불필요한 작업이고,
오래 대기하다가 요청을 보내면 미리 발급해준 토큰이 탈취당할 가능성이 있기 때문에 실제 요청을 보내기 직전에 토큰을 발급하는 방식을 택했다.
어떻게 공격을 방어할 수 있을까?
- 내 서버에 인증된 유저가 위조된 페이지에서 내 서버에 게시물 등록, 비밀번호 변경 등을 요청한다면 먼저 Referer 체크로 공격을 제한할 수 있고, 또 해당 서버에서는 request header에 CSRF 토큰에서 사용하는 쿠키A의 값이 없기 때문에 요청을 제한할 수 있다.
- 게다가 XSS 공격을 당했을 경우에도, 숨겨진 input값이 강제로 submit되어도 쿠키에 담겨진 CSRF토큰값이 존재하지 않으므로 공격을 막을 수 있다.
4. 정리 (+ 주의할 점)
- 먼저 CSRF 공격 동작을 이해하는데 시간이 들었다.. 꽤 오래.. 지금 구현한 방법이 정답은 아니지만 CSRF공격을 막는데 도움이 될 것이다.
- 만약 XSS 공격을 당해 내 사이트에 input값이 숨겨져 있다면 유저가 정상 동작하는 submit버튼을 눌렀을 때, 서버가 공격당할 가능성이 있다. 따라서 CSRF를 막기 위해서 XSS를 막는것도 중요한 일이다.
- 서버에서 쿠키A의 값을 request header에 특정 값을 통해 읽어야 한다. (쿠키값을 그대로 읽으면 당연히 인증이 되어버리니까)
- 두개의 쿠키 모두 유효기간을 아주 짧은 시간으로 잡아두는 것이 좋다. (나는 5초로 잡아두었다)
- Client코드에서 아래처럼 postForm이 실행되기 전 CSRF방어 토큰을 발급받는 api가 실행되야 한다.
postForm : async function () { await getCsrf(); await postForm(); }
참고자료 😃
https://junhyunny.github.io/information/security/spring-boot/spring-security/cross-site-reqeust-forgery/
https://swk3169.tistory.com/24
https://portswigger.net/web-security/csrf
https://sj602.github.io/2018/07/14/what-is-CSRF/
https://nesoy.github.io/articles/2018-06/Java-RegExp
https://nordvpn.com/ko/blog/xss-attack/
https://noirstar.tistory.com/266
https://leonkong.cc/posts/xss.html
'Web' 카테고리의 다른 글
[Web] 웹 스토리지 (localStorage & sessionStorage) (0) | 2024.04.02 |
---|---|
[Web] Web Server & WAS (0) | 2024.03.30 |
[Web] RESTful API (0) | 2024.03.10 |
[Web] LocalStorage & SessionStorage 개념 및 사용법 (0) | 2024.03.10 |
[Web] CORS 개념 및 설정 (0) | 2024.03.09 |