본문 바로가기

보안/시큐어코딩

[시큐어코딩실습] CSRF 방어코드 작성기법


[취약점 진단] 게시판 글쓰기 기능에서 CSRF 취약점이 있는지 진단한다.


아래 openeg_CSRF.txt 파일을 다운로드 받아서 내용을 게시판 글쓰기 기능에서 글 내용으로 사용하여 게시글을 작성한다.

 

openeg게시판에서 CSRF공격 스크립트 샘플


openeg_CSRF.txt




 

[시큐어코딩실습] Spring MVC 프레임워크의 컴포넌트를 이용한 CSRF 공격 차단 



CSRFInterceptor.java

CSRFRequestDataValueProcessor.java

CSRFTokenManager.java


3개의 파일을 다운로드 받아 kr.co.openeg.lab.common.interceptor 패키지에 복사해 넣는다.



---------------------------------------------------------------------------

CSRFTokenManager.java

---------------------------------------------------------------------------


CSRFTokenManager.java 클래스는 세션에 저장된 CSRF토큰을 읽어오는 getTokenForSession() 과 파라미터로 전달되는 CSRF토큰을 읽어오는  getTokenFromRequest() 을 정의하고 있다.


static String getTokenForSession (HttpSession session) { String token = null; // 세션에서 하나의 CSRF 토큰을 만들어서 사용하기 위해 // session에 대해 동기화를 설정하고 // 세션에 CSRF 토큰이 저장되어 있지 않은 경우에 새로운 CSRF 토큰을 생성하여   // 세션에 저장한다. 이미 CSRF 토큰이 생성되어 세션에 저장되어 있다면 저장된 // 세션에 저장되어 있는 CSRF 토큰을 이용한다. synchronized (session) { token = (String) session.getAttribute(CSRF_TOKEN_FOR_SESSION_ATTR_NAME); if (null==token) { token=UUID.randomUUID().toString(); session.setAttribute(CSRF_TOKEN_FOR_SESSION_ATTR_NAME, token); } } return token; }



getTokenForSession 메서드는 존재하는 HTTPSession의 속성으로 저장된 CSRF토큰을 체크한다. 세션 토큰에 의해 생성되어진 값과 요청들어온 값이 같아야 한다. 만약값이 다르다면 요청하는 사용자의 세션은 더 이상 유효하지 않게된다.  토큰값은 random한 값을 사용한다.




---------------------------------------------------------------------------

CSRFRequestDataValueProcess.java 

---------------------------------------------------------------------------


org.springframework.web.servlet.support.RequestDataValueProcessor인터페이스의  getExtraHiddenFields() 메서드를 재정의하여 response의 hidden값으로 CSRF토큰을 전달하도록 구현하였다.



public class CSRFRequestDataValueProcessor implements RequestDataValueProcessor { ... ... @Override public Map<String,String> getExtraHiddenFields(HttpServletRequest request) { Map<String,String> hiddenFields = new HashMap<String,String>(); hiddenFields.put(CSRFTokenManager.CSRF_PARAM_NAME, CSRFTokenManager.getTokenForSession(request.getSession())); return hiddenFields; } }



[Spring 컨테이너 설정 파일 수정] 해당 프로세스가 호출될 수 있도록 스프링 컨텐이너에   requestDataValueProcessor 인스턴스 이름으로 등록한다.

<!-- Data Value Processor --> <bean name="requestDataValueProcessor" class="kr.co.openeg.lab.common.interceptor.CSRFRequestDataValueProcessor"/>



[View 페이지 수정]  클라이언트에서 수신한 CSRF토큰값을 <form>의 히든 필드값으로 포함시키기 위해 기본 HTML의 <form> 대신 Spring의 <form:form> 태그를 사용하도록 View 페이지인 JSP 파일을 수정한다.




<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>



<form:form action="write.do" method="post" onsubmit="return writeFormCheck()" enctype="multipart/form-data">

....

</form:form>




---------------------------------------------------------------------------

CSRFInterceptor.java

---------------------------------------------------------------------------


POST  요청에 대해 서버에서 전송한 CSRF토큰값이 포함되어 요청이 들어왔는지 확인한다.



public class CSRFHandlerInterceptor extends HandlerInterceptorAdapter { ... @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!request.getMethod().equalsIgnoreCase("POST") ) { // Not a POST - allow the request return true; } else { // This is a POST request - need to check the CSRF token String sessionToken = CSRFTokenManager.getTokenForSession(request.getSession()); String requestToken = CSRFTokenManager.getTokenFromRequest(request); if (sessionToken.equals(requestToken)) { return true; } else { response.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad or missing CSRF value"); return false; } } }



[Spring 컨테이너 설정 파일 수정] CSRFInterceptor가 적용되도록  Spring 설정파일(dispatcher-servlet.xml)에  다음과 같이 등록한다. 


<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" ... ... ...  <!-- Interceptor handlers --> <mvc:interceptors> <bean class="com.eyallupu.blog.springmvc.controller.csrf.CSRFHandlerInterceptor"/> </mvc:interceptors> </beans>



 

 

 

[실습2] 각각의 View 페이지(JSP)에서 CSRF 토큰을 첨부하여 응답하도록 코드 작성


 


CSRFInterceptor.java

dispatcher-servlet.xml

write.jsp

 

STEP 1.  성공적으로 로그인이 수행되면 사용자세션에 CSRF 토큰을 생성하여 저장한다.

 

@RequestMapping(value="/login.do", method = RequestMethod.POST) 
public ModelAndView loginProc(@ModelAttribute("LoginModel") LoginSessionModel loginModel,
BindingResult result, HttpSession session, HttpServletRequest request) {
....
if(loginCheckResult == null){
    // 로그인 실패시 처리 
} else {
   // 로그인 성공시 세션에 로그인사용자 정보와 CSRF 토큰값을 생성하여 저장한다.
   
session.setAttribute("userId", userId);
   session.setAttribute("userName",                      
                            loginCheckResult.getUserName());
  session.setAttribute("CSRF_TOKEN",
                            UUID.randomUUID().toString());
  
mav.setViewName("redirect:/board/main.do");
  
return mav;
 }

 

 

STEP 2.  요청한 기능을 처리할 뷰페이지(jsp)에 히든값으로 생성된 CSRF토큰을 전달한다.

 

<input type="hidden"  name="csrf" value="${CSRF_TOKEN}" />

 

 

STEP 3.  POST 방식으로 들어오는 요청에 대해 Interceptor에서 세션에 저장된 CSRF토큰값과 요청파라메터에 전달된 CSRF 토큰값을 비교한다.

             같으면 --> 해당 요청을 Controller에게 전달

             같지않으면 --> 에러처리하고 요청한 기능을 처리하는 페이지로 이동한다.

 

package kr.co.openeg.sec.lab.common.interceptor;

import java.util.Enumeration;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse; 

import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

 

public class CSRFInterceptor extends HandlerInterceptorAdapter{
 @Override
 public boolean preHandle(HttpServletRequest request,
                 HttpServletResponse response, Object handler) throws Exception {
 
  if ( ! request.getMethod().equalsIgnoreCase("post")) {
     return true;
  } else {
     if( request instanceof MultipartHttpServletRequest) {
             Enumeration<String> names =
                               request.getParameterNames();
             while( names.hasMoreElements()) {
                String param=names.nextElement();
                if( param.equals("csrf")) {
                    String value=request.getParameter(param);
                    if ( value != null &&
                        value.equals(request.getSession()
                            .getAttribute("CSRF_TOKEN"))) {

                         return true;
                    }
                 }
              }

       }
  }
  request.getSession().setAttribute("csrfError", "true");
  response.sendRedirect("write.do");
  return false;
 } 

 

 

STEP 4.  환경설정파일(dispatcher-servlet.xml)파일에 Interceptor를 등록한다.

 

<mvc:interceptors>
   <mvc:interceptor>
        <mvc:mapping path="/board/write.do"/>
        <bean class="kr.co.openeg.sec.lab.common.interceptor.CSRFInterceptor"/>
   </mvc:interceptor>
</mvc:interceptors>

 

 

STEP 5.  요청한 기능 페이지에서 에러메시지를 출력하고 값이 입력되기를 대기한다.

 

 <c:if test="${csrfError != null }">
   <script>alert("CSRF 공격이 차단됨");</script>
   <c:remove var="csrfError"/>
</c:if>

 

 

 

 

[원문] Spring MVC를 사용한 간단한 CSRF 방어


Simple CSRF Protection with Spring MVC

By Andy Summers

출처: http://www.gumtree.com/devteam/2014-04-16-simple-csrf-protection.html

Cross-Site Request Forgery (CSRF, also known as XSRF) is one of the most common security vulnerabilities found in websites that involve user authentication in order to view or manipulate sensitive data.

Most web-savvy readers will be familiar with what CSRF is and how it works. For those who aren't, here is the briefest of outlines.

Say you log into your favourite website, your bank for example, and you tick the option for them to remember you so that next time you visit you don't have to re-login again. Usually this is achieved with cookies, so that any request to that site sends the cookies with it and that's how they know who you are. That website may have particular URLs, such that sending a request directly to them could perform some action such as sending money or changing your details.

Now say you are on a different website, and there is a link that you are encouraged to click. The author of this other website could have been sneaky and made it so that clicking this link actually fires a particular request at one of the aforementioned URLs on the other website you'd logged in on. And in doing so you've just inadvertently sent money to the bad guys, or deactivated your account on that site, or reset your password, or similar.

If your web application is written with Spring MVC, there is the very comprehensive security module which provides a framework for authentication, as well as built-in safeguard for CSRFamong many other things. It is an excellent library which will probably suit the needs of most Spring web applications. If however you don't wish to integrate this library - maybe your login model doesn't fit well, or you have no use for the large platter of features it provides, then read on.

There are a few different ways to deal with CSRF, and one is by using a temporary tokenwhich means a submitted request (intended or otherwise) must include with it a token which is checked before letting the request proceed.

The solution I would like to outline assumes the page you would like to protect from CSRF follows the pattern of having a page which is displayed via a GET method, then submitted via a POST.

Step 1. Write annotation and a request interceptor

Annotation

/**
 * Annotation to mark a controller handler as requiring secure token checking
 */
@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface SecureToken {    String TOKEN_PARAMETER_NAME = "secureToken";

    String value();
}

Interceptor

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class SecureTokenInterceptor extends HandlerInterceptorAdapter {    private SecureTokenService secureTokenService;

    /**
     * Constructor.
     *
     * @param secureTokenService - service to store/retrieve tokens
     */
    public SecureTokenInterceptor(SecureTokenService secureTokenService) {        this.secureTokenService = secureTokenService;
    }

    @Override    public boolean preHandle(HttpServletRequest req,                             HttpServletResponse resp,                             Object handler) throws Exception {        HandlerMethod method = (HandlerMethod) handler;        SecureToken secureToken =
                method.getMethodAnnotation(SecureToken.class);
        if (secureToken != null
                && RequestMethod.POST
                    == RequestMethod.valueOf(req.getMethod())) {            return checkValidToken(req, resp, secureToken);
        }

        return true;
    }

    private boolean checkValidToken(HttpServletRequest req,                                    HttpServletResponse resp,                                    SecureToken secureToken)             throws IOException, ModelAndViewDefiningException {        String tokenValue =
                req.getParameter(SecureToken.TOKEN_PARAMETER_NAME);
        if (StringUtils.isEmpty(tokenValue)) {            resp.sendError(400);            // Bad page setup or someone is messing with us            return false;        } else if (!secureTokenService.checkToken(                secureToken.value(), tokenValue)) {            ModelAndView mav =                     new ModelAndView("redirect:/page-expired");            throw new ModelAndViewDefiningException(mav);        }

        return true;
    }

@Override public void postHandle(HttpServletRequest req,  HttpServletResponse resp, Object handler,  ModelAndView modelAndView)  throws Exception { HandlerMethod method = (HandlerMethod) handler; SecureToken secureToken =  method.getMethodAnnotation(SecureToken.class); if (secureToken != null) { String tokenValue = secureTokenService .getOrGenerateToken(secureToken.value()); modelAndView.addObject( SecureToken.TOKEN_PARAMETER_NAME, tokenValue); } } }

You'll see the interceptor refers to a SecureTokenService to store/retrieve the tokens. This can use whatever you want really - a database, some kind of cache, or session (though at Gumtree we prefer to avoid using sessions)

Step 2. Wire the interceptor in via spring config

@Configurationpublic class SellerWebsiteInterceptorsConfig                  extends WebMvcConfigurerAdapter {    @Autowired    private SecureTokenService secureTokenService;
    @Override    public void addInterceptors(InterceptorRegistry registry) {        // Put your other interceptors here too ….
        registry.addInterceptor(secureTokenInterceptor());    }

    @Bean
    public HandlerInterceptor secureTokenInterceptor() {        return new SecureTokenInterceptor(secureTokenService);
    }
}

Step 3. Annotate your controller methods

@Controller@RequestMapping(PAGE_PATH)public final class DangerousPageController {    @SecureToken(value = VIEW_NAME)    @RequestMapping(method = RequestMethod.GET)    public String showPage() {        // Show the page…
    }

@SecureToken(value = VIEW_NAME) @RequestMapping(method = RequestMethod.POST) public String submitPage() { // Process the submit } }

Step 4. Put the token in the page

<form action="..." method="post">
    ...    <input type="hidden" name="secureToken" value="${secureToken}"/></form>

That's it really. If you feel this is not foolproof enough there are further steps you can take, such as encrypting the token based on who the user is, or mixing up the name of the token parameter - again you could use some encryption technique for this, or just periodically change it. Either way, doing this gives your web application at least some level of safety against the bad guys.