서버측 구현 방안
서버측 CSRF 방어 로직
CSRF 방어를 위한 로직을 정리하면 다음과 같다.
- 검증할 요청인지 확인하여 검증할 요청이 아니면,
- 이미 Session에 생성된 Token이 없으면 Token을 생성하고 Session에 저장한다.
- 처리를 종료한다.
- 제출된 Token이 없으면
- 제출 Token 없음으로 처리한다.
- Session에 저장된 Token이 없으면
- Session에 Token이 없음으로 처리한다.
- 제출된 Token과 Session에 저장된 Token이 동일하지 않으면
- Token 불일치로 처리한다.
- 제출된 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>의 파라미터 삽입
- <head>의 <meta>태그에 포함.
클라이언트가 받은 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 공격 방어용 토큰 생성 및 세션 저장
- 클라이언트 요청에 대해 CSRF 공격 방어 토큰 확인
- 클라이언트에서 요청시 토큰을 전달하도록 코드 적용
- HTML 응답을 요구하는 요청 처리
- 폼 제출 등 전통적인 요청
- <a>~</a> 링크 태그
- AJAX 요청 처리
- HTML 응답을 요구하는 요청 처리
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"키로 저장된다
- 기본값: "_csrf"
- csrfHeaderName: CSRF 토큰 헤더명
- 기본값: "X-CSRF-Token"
- 생성된 CRSF Token 값을 담는 헤더 명칭과 요청 헤더에 담긴 CSRF Token값을 얻을 때 사용한다.
- ServletContext에 "_csrf.name" 키로 저장된다
- 생성된 토큰값을 ServletContext에 "_csrf.token"키로 저장된다
- 기본값: "X-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 처리에 적용
- GET, POST, PUT, DELETE: