IT

스프링시큐리티인액션 챕터10

프로개발러 2023. 8. 17. 16:52
반응형

크로스 사이트 요청 위조

 

크로스 사이트 요청 위조(CSRF)는 인증된 사용자가 웹 애플리케이션에 특정 요청을 보내도록 유도하는 공격 행위를 말합니다. 크로스 사이트 요청 위조는 생성된 요청이 사용자의 동의를 받았는지 확인할 수 없는 웹 애플리케이션의 CSRF 취약점을 이용합니다. 

 

 

예전 옥션 해킹 사건을 기준으로 예시를 들어보겠습니다.

 

...
<img src="http://auction.com/changeUserAcoount?id=admin&password=admin" width="0" height="0">
...

 

  1. 옥션 관리자가 관리 권한을 가지고(이미 유효한 쿠키 발급된 상태로) 메일을 조회합니다.
  2. 해커는 위와 같이 태그가 들어간 코드가 담긴 이메일을 보낸다. 단, 관리자는 이미지 크기가 0이므로 이미지가 있는지 눈치를 채지 못합니다.
  3. 관리자가 메일을 열면 이미지 파일을 받아오기 위해 URL이 열리게 됩니다.
  4. 그렇게 되면 해커가 원하는 대로 관리자의 계정이 id와 pw 모두 admin인 계정으로 변경된다고 합니다.

https://coding-nyan.tistory.com/124

 

CSRF 공격이란?

요즘 스프링 시큐리티를 적용한 뒤로 자꾸 CSRF 관련으로 문제가 생겼어요. 얘가 대체 뭐길래!! 저를 이렇게 고통스럽게 하는지 궁금해서 오늘은 CSRF 공격에 대해서 다뤄보겠습니다. CSRF란? Cross-si

coding-nyan.tistory.com

 

 

CSRF 방어 기법

  • Referer 체크
    • HTTP 헤더에 있는 Referer로 해당 요청이 요청된 페이지의 정보를 확인하는 방법. 간단하여 소규모 웹사이트에 주로 이용된다. 하지만, 해당 정보는 Paros나 Zap, fiddler같은 프로그램으로 조작이 가능하다.
  • GET / POST 요청 구분
    • img 태그 등의 경우 GET 요청으로, form 태그로 값을 받을 경우 POST를 이용하여 요청을 구분해준다.
  • Token 사용
    • 서버에서 hash로 암호화 된 token을 발급, 사용자는 매 요청마다 token을 함께 보내어 서버의 검증을 거쳐야한다.
  • 추가 인증 수단 사용 ( ex. CAPCHA )
    • 추가 인증 수단을 거쳐 만약 테스트를 통과하지 못할 시, 해당 요청 자체를 거부하는 방법.

 

 

 

 

 

configuration 파일

addfilterafter를 통해서 csrftokenlogger를 csrffilter다음에 필터 추가

 

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(
                new CsrfTokenLogger(),
                CsrfFilter.class)
                .authorizeRequests()
                    .anyRequest().permitAll();
    }
}

 

컨트롤러

 

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String getHello() {
        return "Get Hello!";
    }

    @PostMapping("/hello")
    public String postHello() {
        return "Post Hello!";
    }
}

 

csrftokenlogger 소스

public class CsrfTokenLogger implements Filter {

    private Logger logger =
            Logger.getLogger(CsrfTokenLogger.class.getName());

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        Object o = request.getAttribute("_csrf");
        CsrfToken token = (CsrfToken) o;
        logger.info("CSRF token " + token.getToken());

        filterChain.doFilter(request, response);
    }
}

 

get방식으로 호출시에

 

curl -v localhost:8080/hello

 

아래와 같이 결과값 나옴.

 

*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.0.1
> Accept: */*
>
< HTTP/1.1 200
< Set-Cookie: JSESSIONID=5E54F81FCC4391E4441277ADDE7E256C; Path=/; HttpOnly
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 10
< Date: Thu, 17 Aug 2023 05:26:16 GMT
<
Get Hello!* Connection #0 to host localhost left intact

 

호출할 때마다 jsessionid값이 바뀌고 csrf토큰이 새로 생성되는 것을 볼 수 있음.

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.addFilterAfter(
                new CsrfTokenLogger(),
                CsrfFilter.class)
                .authorizeRequests()
                    .anyRequest().permitAll();
    }
}

 

이런식으로 csrf disable해주면

 

아래와 같이 csrf token null오류발생.

Cannot invoke "org.springframework.security.web.csrf.CsrfToken.getToken()" because "token" is null

 

참고로 csrf disable해줘도

http.addFilterAfter(
        new CsrfTokenLogger(),
        CsrfFilter.class)

 

이 부분은 작동함.

 

csrf적용했을 때 필터 추천


Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CsrfFilter
  CsrfTokenLogger
  LogoutFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]

 

 

csrf disable했을 때 필터체인


Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CsrfTokenLogger
  LogoutFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]

 

post요청시에

cmd에서 curl로 잘 안되서 postman 사용

x-csrf-token의 유효타임시간이 있는것 같다...

 

jsessionid는 내가 임의로 지정해줌.

 

 

정상 응답일 경우

 

 

csrf token의 맨뒷자리를 하나만 바꿔주면 바로 403 error 발생.

 

token validation check하는 부분

public final class CsrfFilter extends OncePerRequestFilter {

체크하는 로직

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;
}

 

참고로 동일한 jsessionid에 대해서 동일한 csrf token이 맵핑됨.

 

이 부분이 책에서 정확하게 나오지 않았는데

csrf token 발급 하는 부분은

 

csrffilter클래스 보면 주석에

* <p>
* Typically the {@link CsrfTokenRepository} implementation chooses to store the
* {@link CsrfToken} in {@link HttpSession} with {@link HttpSessionCsrfTokenRepository}
* wrapped by a {@link LazyCsrfTokenRepository}. This is preferred to storing the token in
* a cookie which can be modified by a client application.

같이 설명이 나옴.

public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {

아무리 찾아도 csrftoken generate하는 부분만 있고 실제로 구현하는 부분을 못 찾았는데

위의 클래스에 숨어있음.

 

generatetoken부분

/*
 * (non-Javadoc)
 *
 * @see org.springframework.security.web.csrf.CsrfTokenRepository#generateToken(javax.
 * servlet .http.HttpServletRequest)
 */
public CsrfToken generateToken(HttpServletRequest request) {
   return new DefaultCsrfToken(this.headerName, this.parameterName,
         createNewToken());
}

createnewtoken보면 uuid로 랜덤uuid값 가져옴.

 

private String createNewToken() {
   return UUID.randomUUID().toString();
}

랜덤 uuid 값 만드는 본체

 

}

/**
 * Static factory to retrieve a type 4 (pseudo randomly generated) UUID.
 *
 * The {@code UUID} is generated using a cryptographically strong pseudo
 * random number generator.
 *
 * @return  A randomly generated {@code UUID}
 */
public static UUID randomUUID() {
    SecureRandom ng = Holder.numberGenerator;

    byte[] randomBytes = new byte[16];
    ng.nextBytes(randomBytes);
    randomBytes[6]  &= 0x0f;  /* clear version        */
    randomBytes[6]  |= 0x40;  /* set to version 4     */
    randomBytes[8]  &= 0x3f;  /* clear variant        */
    randomBytes[8]  |= 0x80;  /* set to IETF variant  */
    return new UUID(randomBytes);
}

 

 

그다음에는 html화면에 csrf토큰값이 있는지 체크하는 예제

 

참고로 스프링시큐리티는 기본이 csrf체크이기 때문에

form에 csrm토큰 값이 있어야 함.

 

configure 부분

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated();

    http.formLogin()
        .defaultSuccessUrl("/main", true);
}

 

main.html 부분

 

<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
    </head>
    <body>
        <form action="/product/add" method="post">
            <span>Name:</span>
            <span><input type="text" name="name" /></span>
            <span><button type="submit">Add</button></span>
            <input type="hidden"
                    th:name="${_csrf.parameterName}"
                    th:value="${_csrf.token}" />
        </form>
    </body>
</html>

csrf token을 hidden value값으로 넘겨줌.

 

 

 

그다음 예제는 실제 토큰을 구성하는 방법을 직접 구현하는 것을 테스트

 

여기서 몰랐던 부분은 csrf에서 특정 url을 무시하도록 람다식으로 처리

 

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf(c -> {
        c.csrfTokenRepository(customTokenRepository());
        c.ignoringAntMatchers("/ciao");

 

커스텀csrftoken리파지토리를 만들어서 csrftoken리파지토리를 직접 구현해서 오버라이딩 하는 부분

 

public class CustomCsrfTokenRepository implements CsrfTokenRepository {

 

크게 토큰 생성,로드,저장(db)하는 부분으로 나뉨.

 

 

 

보통 여기서 끝나는데 cors도 추가로 있음.

 

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

 

교차 출처 리소스 공유 (CORS)

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.

교차 출처 요청의 예시: https://domain-a.com의 프론트 엔드 JavaScript 코드가 XMLHttpRequest를 사용하여 https://domain-b.com/data.json을 요청하는 경우.

보안 상의 이유로, 브라우저는 스크립트에서 시작한 교차 출처 HTTP 요청을 제한합니다. 예를 들어, XMLHttpRequest와 Fetch API 동일 출처 정책을 따릅니다. 즉, 이 API를 사용하는 웹 애플리케이션은 자신의 출처와 동일한 리소스만 불러올 수 있으며, 다른 출처의 리소스를 불러오려면 그 출처에서 올바른 CORS 헤더를 포함한 응답을 반환해야 합니다.

 

 

 

이 글은 누가 읽어야 하나요?

모든 사람이요, 진짜로.

 

 

컨트롤러 부분

 

@Controller
public class MainController {

    private Logger logger =
            Logger.getLogger(MainController.class.getName());

    @GetMapping("/")
    public String main() {
        return "main.html";
    }

    @PostMapping("/test")
    @ResponseBody
//    @CrossOrigin("http://localhost:8080")
    public String test() {
        logger.info("Test method called");
        return "HELLO";
    }
}

 

cors 를 해결하는 가장 간단한 방법중 하나는 cors 어노테이션 추가

 

 

Controller 에서 @CrossOrigin 어노테이션 추가하기

아래와 같이 @CrossOrigin을 통해 어노테이션 추가

@CrossOrigin("http://localhost:8080")
public String test() {
    logger.info("Test method called");
    return "HELLO";
}

 

참고로 이 예제에서는 localhost로 서버를 띄우고 127.0.0.1로 호출하도록 교차출처 테스트를 함.

<!DOCTYPE HTML>
<html lang="en">
    <head>
        <script>
            const http = new XMLHttpRequest();
            const url='http://127.0.0.1:8080/test';
            http.open("POST", url);
            http.send();

            http.onreadystatechange = (e) => {
                document.getElementById("output")
                    .innerHTML =
                    http.responseText;
            }
        </script>
    </head>
    <body>
        <div id="output"></div>
    </body>
</html>

 

이 부분은 교차출처 허용을 람다식을 통해서 설정하는 부분

@Override
protected void configure(HttpSecurity http) throws Exception {
    
    http.cors(c -> {
        CorsConfigurationSource source = request -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedOrigins(List.of("*"));
            config.setAllowedMethods(List.of("*"));
            return config;
        };
        c.configurationSource(source);
    });
    
    http.csrf().disable();

    http.authorizeRequests()
            .anyRequest().permitAll();
}

 

origins에다가 도메인을 넣고 methods에다가 put,get,delete등을 넣음...

 

반응형

'IT' 카테고리의 다른 글

spring security in action chapter 11  (0) 2023.08.25
백준 파이썬 10845 큐  (0) 2023.08.22
스프링 시큐리티 인 액션 9장  (0) 2023.08.12
스프링시큐리티 인액션 8장  (0) 2023.08.04
파이썬 1260번  (0) 2023.08.01