서버측 구현 방안

4. 1 오후 1:48

서버측 CSRF 방어 로직

CSRF 방어를 위한 로직을 정리하면 다음과 같다.

  1. 검증할 요청인지 확인하여 검증할 요청이 아니면,
    • 이미 Session에 생성된 Token이 없으면 Token을 생성하고 Session에 저장한다.
    • 처리를 종료한다.
  2. 제출된 Token이 없으면 
    • 제출 Token 없음으로 처리한다.
  3. Session에 저장된 Token이 없으면 
    • Session에 Token이 없음으로 처리한다.
  4. 제출된 Token과 Session에 저장된 Token이 동일하지 않으면
    • Token 불일치로 처리한다.
  5. 제출된 Token과 Session에 저장된 Token이 같으면

Token 검증 및 생성을 처리하는 클래스(CsrfTokenChecker)와  Token 검증 미통과시 처리하는 클래스(CsrfProtector), 그리고 이를 사용하는 클래스(CsrfProtectionFilter, CsrfProtectionInterceptor)를 나누어 구현한다.

CSRF 방어 Token 생성

Token의 최초 생성

사용자의 Session이 생겼을 때 최초 Token을 생성한다.

이게 필요한가?

Token 생성 주기

Token은 설정값(lifecycle?)에 따라 새로운 Token을 생성한다.

  • session: 생성 주기가 Session당 1회
    • 최초 생성 후 계속 사용한다.
  • request: 매 요청당 1회
    • 매 요청마다 Token을 갱신한다.
  • time: 설정 시간(분) 간격당 1회
    • Token 검증 후 설정 시간이 지났으면 갱신한다.

클라이언트에게 Token 전달

클라이언트에게 Token을 전달하는 방법은 다음과 같은 것들이 있을 수 있다.

  • 응답 헤더에 포함하는 방법
    • 값을 사용하려면 JavaScript 처리 필요
  • 응답 쿠키에 포함하는 방법
    • 값을 사용하려면 JavaScript 처리 필요
  • 응답 HTML에 포함하는 방법
    • <head>의 <meta>태그에 포함.
      • 값을 사용하려면 JavaScript 처리 필요
    • <script>내의 특정 변수에 포함
      • 값을 사용하려면 JavaScript 처리 필요
    • 매 <form>의 파라미터 삽입

클라이언트가 받은 Token 사용

HTML로 응답을 받을 때 다음과 같은 <meta> 태그에 Token을 설정.

  • _csrf.token: Token 값
  • _csrf.header: Token 요청, 응답시 전달하는 헤더이름
  • _csrf.parameter: 요청할 때 Token값을 전달하는 파라미터명

Session에 저장되는 매칭되는 변수은 다음과 같다.

  • <meta name="_csrf.token" value="${_csrf.token">
  • <meta name="_csrf.header" value="${_csrf.headerName">
  • <meta name="_csrf.parameter" value="${_csrf.parameterName">

AJAX 호출시 Token 갱신

Token 갱신 주기가 session이 아닌 경우, AJAX 요청에 의해 Token 정보가 변경될 수 있다. 따라서 AJAX 요청에 대한 응답에 변경된 Token 값을 받아 갱신처리가 있어야 한다.

서버에게 Token 전달

기본적으로 POST, PUT, DELETE 메쏘드 호출시에만 Token을 전달한다.

<form> 태그에 적용

<input type="hidden" name="${_csrf.pameterName}" value="${_csrf.token}">

AJAX 호출시 적용

window._fetch = window.fetch;
window.fetch = function(resource, options) {
  var opts = options || {};

  var headerName = document.querySelector('meta["_csrf.header"]')
                     .getAttribute('content');
  var token = document.querySelector('meta["_csrf.token"]')
                    .getAttribute('content');

  opts[headerName] = token;

  return fetch(resource, opts);
};

 

 

적용은 다음과 같은 내용이 필요하다.

  • CSRF 공격 방어 필터 또는 Spring의 Interceptor 적용
    • 클라이언트 요청에 대해 CSRF 공격 방어 토큰 확인
      • 예외 필요(최초 요청 등을 위해)
      • 점검 미통과시 처리
    • CSRF 공격 방어용 토큰 생성 및 세션 저장
  • 클라이언트에서 요청시 토큰을 전달하도록 코드 적용
    • HTML 응답을 요구하는 요청 처리
      • 폼 제출 등 전통적인 요청
    • <a>~</a> 링크 태그
    • AJAX 요청 처리

CSRF 공격 방어 필터 적용

자체 구현 CSRF Filter 를 적용한다.

다음은 적용 예시이다.

<!-- CSRF 공격 방어 Filter -->
<filter>
  <filter-name>csrfFilter<filter-name>
  <filter-class>net.waglewagle.servlet.filter.CsrfFilter</filter-class>

  <!-- CSRF 공격 방어 토큰 파라미터 이름 -->
  <init-param>
    <param-name>csrfParamName</param-name>
    <param-value>_csrf</param-value>
  </init-param>

  <!-- CSRF 공격 방어 토큰 헤더 이름 -->
  <init-param>
    <param-name>csrfHeaderName</param-name>
    <param-value>X-CSRF-Token</param-value>
  </init-param>

  <!-- 필터 검증 미통과시 응답 코드 -->
  <init-param>
    <param-name>reponse</param-name>
    <param-value>400</param-value>
  </init-param>

  <!-- 필터 검증 적용 기본 정책: 
      CHECK - 모두 검증 후 통과
      UNCHECK - 모두 검증없이 통과 -->
  <init-param>
    <param-name>policy</param-name>
    <param-value>CHECK</param-value>
  </init-param>

  <!-- 필터 검증 적용 기본 정책 예외 목록 -->
  <init-param>
    <param-name>exclude</param-name>
    <param-value>
      [GET,HEAD]
      /membership/
      ?memberId&password
    </param-value>
  </init-param>
</filter>

<filter-mapping>
  <filter-name>csrfFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

필터 초기 파라미터

  • csrfParamName: CSRF 토큰 파라미터명
    • 기본값: "_csrf"
      • CRSF Token 값을 갖는 <meta>태그 name 속성에 사용된다.
    • ServletContext에 "_csrf.name" 키로 저장된다
      • 생성된 토큰값을 ServletContext에 "_csrf.token"키로 저장된다
  • csrfHeaderName: CSRF 토큰 헤더명
    • 기본값: "X-CSRF-Token"
      • 생성된 CRSF Token 값을 담는 헤더 명칭과 요청 헤더에 담긴 CSRF Token값을 얻을 때 사용한다.
    • ServletContext에 "_csrf.name" 키로 저장된다
      • 생성된 토큰값을 ServletContext에 "_csrf.token"키로 저장된다
  • response: 점검을 통과하지 못 했을 때 응답으로 보낼 HTTP 응답 코드
    • 기본값 400(Bad Request)
  • policy: CSRF 필터 검증 기본 정책
    • CHECK: 모든 요청에 대해 검증후 허용한다.
    • UNCHECK: 모든 요청에 대해 검증없이 허용한다.
  • exclude: 검증 기본 정책의 예외
    • 한 줄에 하나씩 입력
      • 앞뒤 공백은 무시됩니다.
      • 나열한 목록에 하나라도 해당되면 예외로 인정됩니다.
    • 형식: {[메쏘드목록]}{URL패턴}?{파라미터들}
      • 예시
        • [GET,HEAD]: 요청 메쏘드가 GET 또는 HEAD인 요청
        • [POST]/member/login.do?a&b=v1
          • "/member/login.do" URL로 파라미터명 "a"가 있고, 파라미터 "b"의 값이 "v1"인 POST 요청
        • /member/login.do: "/member/login.do" 일치
        • /login.*: 확장자를 가진 "/login."으로 시작하는 URL
        • /a/**: "/a/"으로 시작하는 모든 URL
          • "/a/b", "/a/b.do", "/a/b/c.view", "/a/b/c.x" 등
      • {[메쏘드목록]}: (선택항목) 예외 메쏘드 목록. 대소문자는 구별하지 않는다.
      • {URL패턴}: 파라미터를 제외한 요청 URL 패턴
        • **: 경로 구분자(/)를 포함한 0번 이상의 모든 문자
        • *: 경로 구분자(/)를 제외한 0번 이상의 모든 문자
      • {파라미터들}: {파라미터명}={파라미터값들}, "&"로 구별하여 여러파라미터 가능
        • {파라미터명}: 알파벳으로 시직하는 알파펫과 숫자와 언더바(_), 대쉬(-)로 구성
        • {파라미터값들}: "^"를 구별자로 AND 조건. "|"를 구별자로 OR 조건. 동일 파라미터명을 여러 개 있는 경우 OR 조건.
          • a=1^2: 파라미터 "a" 값이 "1"과 "2"가 모두 있는 경우
          • a=1|2&a=3: 파라미터 "a" 값이 "1"이거나 "2" 또는 "3"인 경우

클라이언트에서 요청시 토큰을 전달하도록 코드 적용

CRSF 파라미터를 자동 삽입해 주는 JavaScript 파일 정의

<script src="/js/crsf.js?name=_csrf&include=AJAX,POST"></script>
// 또는
<script src="/js/crsf.js?name=_csrf&exclude=GET"></script>

해당 JavaScript는, <meta> 태그에 정의된 토큰값을 읽어 적용.

  • <form> 태그를 찾아 CSRF 토큰값을 가진 <input type="hidden"> 태그 삽입
  • fetch() 전역 함수를 "X-CSRF-Token" 헤더 삽입하도록 수정 
  • jquery의 ajax의 전체 요청 전 훅 메쏘드에 "X-CSRF-Token" 헤더 삽입
  • 기타 추후 적용되는 fetch 대체 AJAX 요청에 "X-CSRF-Token" 헤더 삽입하도록 함

[파라미터]

  • name: CSRF 토큰값을 읽어 올 메타태그명
    • 기본값: "_csrf"
  • include 또는 exclude
    • 포함 또는 예외. 둘 중 하나만 있어야 함.
    • 둘 다 있을 경우, include만 적용
    • 값: 대소문자 구별하지 않음
      • GET, POST, PUT, DELETE:
        • 적용할 form의 method 속성에 정의된 메쏘드값. 없으면 모든 form 태그에 적용
        • 단, 동적으로 생성되는 form인 경우는 별도로 적용하는 코드를 작성해야 함.
      • LINK: <a> 태그 클릭시 처리. 단 onclick이 정의되어 있거나 다른 click 이벤트 핸들러가 정의되어 있는 경우, href에 해쉬값만 있는 경우는 적용하지 않음.
      • AJAX: ajax 처리에 적용