Closed binchoo closed 2 years ago
meta
<meta name="csrf-token" content="{{#_csrf}}token{{/_csrf}}">
form
_csrf
<html> <body> <form method="POST" enctype="multipart/form-data" action="/넘길페이지"> <div> <input type="hidden" name="_csrf" value="{{#_csrf}}token{{/_csrf}}" /> <input type="submit" value="Upload" /> </div> </form> </body> </html>
RESTful 서버는 뷰 렌더링을 하지 않으므로, CSRF 토큰을 획득할 수 있는 별도 API를 클라이언트에게 제공합니다.
사례: SAP Netweaver API 문서 요청: GET /mcm/json 헤더: X-CSRF-Token: fetch
GET /mcm/json
X-CSRF-Token: fetch
X-CSRF-Token
클라이언트(자바 스크립트)의 대응
jQuery.ajax("/mcm/json",{ type: "GET", contentType: 'application/json', dataType: 'json', beforeSend: function(xhr){ xhr.setRequestHeader('X-CSRF-Token', 'fetch'); }, complete : function(response) { jQuery.ajaxSetup({ beforeSend: function(xhr) { xhr.setRequestHeader("X-CSRF-Token",response.getResponseHeader('X-CSRF-Token')); } }); } });
클라이언트(자바)의 대응
public class CustomAuthenticationProvider extends StandardAuthenticationProvider { private String token = "fetch"; @Override public Map<String, List<String>> getHTTPHeaders(String url) { Map<String, List<String>> httpHeaders = super.getHTTPHeaders(url); if(httpHeaders==null) { httpHeaders = new HashMap<String, List<String>>(); } httpHeaders.put("X-CSRF-Token", Collections.singletonList(token)); return httpHeaders; } @Override public void putResponseHeaders(String url, int statusCode, Map<String, List<String>> headers) { super.putResponseHeaders(url, statusCode, headers); if(headers!=null) { for(String headerName:headers.keySet()) { // loop for a ignore case check -> header names are case-insensitive (RFC 2616) if(headerName!=null && headerName.equalsIgnoreCase("X-CSRF-Token") && !headers.get(headerName).isEmpty()) { this.token = headers.get(headerName).get(0); } } } } }
이후 내용으로 충분히 설명됨
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http ... .csrf() .csrfTokenRepository(cookieCsrfRepository()) .and(); } @Bean HttpSessionCsrfTokenRepository sessionCsrfRepository() { HttpSessionCsrfTokenRepository csrfRepository = new HttpSessionCsrfTokenRepository(); // HTTP 헤더에서 토큰을 인덱싱하는 문자열 설정 csrfRepository.setHeaderName("X-CSRF-TOKEN"); // URL 파라미터에서 토큰에 대응되는 변수 설정 csrfRepository.setParameterName("_csrf"); // 세션에서 토큰을 인덱싱 하는 문자열을 설정. 기본값이 무척 길어서 오버라이딩 하는 게 좋아요. // 기본값: "org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" csrfRepository.setSessionAttributeName("CSRF_TOKEN"); return csrfRepository; } @Bean CookieCsrfTokenRepository cookieCsrfRepository() { CookieCsrfTokenRepository csrfRepository = new CookieCsrfTokenRepository(); csrfRepository.setCookieHttpOnly(false); csrfRepository.setHeaderName("X-CSRF-TOKEN"); csrfRepository.setParameterName("_csrf"); csrfRepository.setCookieName("XSRF-TOKEN"); //csrfRepository.setCookiePath("..."); // 기본값: request.getContextPath() return csrfRepository; } }
스프링 시큐리티 설정
csrf().disable()
csrf().csrfTokenRepository(repo)
HttpSessionCsrfTokenRepository
CookieCsrfTokenRepository
LazyCsrfTokenRepository
시큐리티 설정에서 csrf().csrfTokenRepository(sessionCsrfRepository()) 처럼 HttpSessionCsrfTokenRepository를 주입합니다.
csrf().csrfTokenRepository(sessionCsrfRepository())
이제 클라이언트가 임의의 요청을 보내면 CsrfFilter가 끼어들어 CSRF 토큰을 생성하고 HttpSession과 HttpServletRequest에 토큰을 저장합니다.
CsrfFilter
HttpSession
HttpServletRequest
클라이언트 측이 해당 토큰을 얻어야하므로 다음과 같이 컨트롤러를 작성해 봅니다. 클라이언트는 /csrf 주소로 접근하여 응답 헤더의 X-CSRF-TOKEN를 보고 CSRF 토큰을 얻을 수 있습니다.
/csrf
X-CSRF-TOKEN
@RequestMapping("/csrf") @Controller public class CsrfController { private static final Logger logger = LoggerFactory.getLogger(CsrfController.class); @RequestMapping(method = RequestMethod.GET) public ResponseEntity<String> getOrCreateCsrfToken(HttpSession session, HttpServletRequest request) { final DefaultCsrfToken csrfToken = (DefaultCsrfToken) session.getAttribute( "CSRF_TOKEN"); assert(csrfToken == request.getAttribute(csrfToken.getParameterName())); return ResponseEntity.ok() .header(csrfToken.getHeaderName(), csrfToken.getToken()).body("Check your response header!"); } }
시큐리티 설정에서 csrf().csrfTokenRepository(cookieCsrfRepository()) 처럼 CookieCsrfTokenRepository를 주입합니다.
csrf().csrfTokenRepository(cookieCsrfRepository())
이제 클라이언트가 임의의 요청을 보내면 CsrfFilter가 끼어들어 CSRF 토큰을 생성하고 HttpServletRequest에 토큰을 저장합니다. 최초에 한하여 응답 헤더에 Set-Cookie로 CSRF 토큰을 내려 보냅니다.
Set-Cookie
최초 접근 이후 클라이언트는 CSRF 토큰을 쿠키로 보유하는 상태입니다. 서버는 매 (요청 헤더 or 파라미터) vs 쿠키를 비교하여 유효한 CSRF가 담겼는지 검증합니다.
다음 절차로 송수신 되는 패킷을 확인해 봅시다.
XSRF-TOKEN
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(HttpServletResponse.class.getName(), response); CsrfToken csrfToken = this.tokenRepository.loadToken(request); final boolean missingToken = csrfToken == null; if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); this.tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); if (!this.requireCsrfProtectionMatcher.matches(request)) { filterChain.doFilter(request, response); return; } String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!csrfToken.getToken().equals(actualToken)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)); } if (missingToken) { this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken)); } else { this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken)); } return; } filterChain.doFilter(request, response); }
jQuery를 활용 중이라면 모든 요청 헤더에 CSRF 토큰을 포함하도록 할 수 있습니다. 서버와 합의된 헤더 형식을 지켜 CSRF 토큰을 적재합시다. 기본적으로 기대되는 헤더명은 X-CSRF-TOKEN or X-XSRF-TOKEN 입니다.
X-XSRF-TOKEN
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } });
form에 CSRF 토큰을 위한 hidden 필드가 존재한다면 이 방식으로 동작합니다. 기본적으로 서버가 기대하는 파라미터명은 _csrf 입니다.
<html> <body> <form method="POST" enctype="multipart/form-data" action="/넘길페이지"> <div> <input type="hidden" name="_csrf" value="a60159ae-9b7f-45dc-9c97-3a5f14a39cbd" /> <input type="submit" value="Upload" /> </div> </form> </body> </html>
@caffeine-library/readers-pro-spring-5
CSRF 토큰을 통한 보호
Q1. 클라이언트는 어떻게 CSRF 토큰을 얻나요
방법1. 서버가 HTML 렌더링 시
meta
태그에 토큰 집어 넣어 주기방법2. 서버가 HTML 렌더링 시
form
태그에 hidden_csrf
필드 집어 넣어 주기방법3. 서버의 API 호출하기
RESTful 서버는 뷰 렌더링을 하지 않으므로, CSRF 토큰을 획득할 수 있는 별도 API를 클라이언트에게 제공합니다.
사례: SAP Netweaver API 문서 요청:
GET /mcm/json
헤더:X-CSRF-Token: fetch
X-CSRF-Token: fetch
를 포함하여야 한다.X-CSRF-Token
에 CSRF 토큰 값을 담아 응답한다.클라이언트(자바 스크립트)의 대응
클라이언트(자바)의 대응
방법4. 쿠키로 내려받기
이후 내용으로 충분히 설명됨
Q2. 서버는 토큰을 어떻게 생성하나요?
스프링 시큐리티 설정
csrf().disable()
을 하지 않으면 됩니다!csrf().csrfTokenRepository(repo)
토큰 생성하고서 이것을 저장할 장소(Repository)를 골라야 합니다.HttpSessionCsrfTokenRepository
CookieCsrfTokenRepository
LazyCsrfTokenRepository
방법1. 토큰을 만들면 유저의 세션에 저장할래요
시큐리티 설정에서
csrf().csrfTokenRepository(sessionCsrfRepository())
처럼HttpSessionCsrfTokenRepository
를 주입합니다.이제 클라이언트가 임의의 요청을 보내면
CsrfFilter
가 끼어들어 CSRF 토큰을 생성하고HttpSession
과HttpServletRequest
에 토큰을 저장합니다.클라이언트 측이 해당 토큰을 얻어야하므로 다음과 같이 컨트롤러를 작성해 봅니다. 클라이언트는
/csrf
주소로 접근하여 응답 헤더의X-CSRF-TOKEN
를 보고 CSRF 토큰을 얻을 수 있습니다.방법2. 토큰을 만들면 브라우저 쿠키에 저장할래요
시큐리티 설정에서
csrf().csrfTokenRepository(cookieCsrfRepository())
처럼CookieCsrfTokenRepository
를 주입합니다.이제 클라이언트가 임의의 요청을 보내면
CsrfFilter
가 끼어들어 CSRF 토큰을 생성하고HttpServletRequest
에 토큰을 저장합니다. 최초에 한하여 응답 헤더에Set-Cookie
로 CSRF 토큰을 내려 보냅니다.최초 접근 이후 클라이언트는 CSRF 토큰을 쿠키로 보유하는 상태입니다. 서버는 매 (요청 헤더 or 파라미터) vs 쿠키를 비교하여 유효한 CSRF가 담겼는지 검증합니다.
다음 절차로 송수신 되는 패킷을 확인해 봅시다.
XSRF-TOKEN
)CsrfFilter가 하는 일을 알고 싶어요
Q3. 클라이언트는 어떻게 리퀘스트를 전송해야 하나요
방법1. 헤더에 CSRF 토큰을 넣어 보내세요
jQuery를 활용 중이라면 모든 요청 헤더에 CSRF 토큰을 포함하도록 할 수 있습니다. 서버와 합의된 헤더 형식을 지켜 CSRF 토큰을 적재합시다. 기본적으로 기대되는 헤더명은
X-CSRF-TOKEN
orX-XSRF-TOKEN
입니다.방법2. 파라미터에 CSRF 토큰을 넣어 보내세요
form
에 CSRF 토큰을 위한 hidden 필드가 존재한다면 이 방식으로 동작합니다. 기본적으로 서버가 기대하는 파라미터명은_csrf
입니다.연관 챕터
93 #97 Spring Security
@caffeine-library/readers-pro-spring-5