크로스 사이트 요청 위조
크로스 사이트 요청 위조(CSRF)는 인증된 사용자가 웹 애플리케이션에 특정 요청을 보내도록 유도하는 공격 행위를 말합니다. 크로스 사이트 요청 위조는 생성된 요청이 사용자의 동의를 받았는지 확인할 수 없는 웹 애플리케이션의 CSRF 취약점을 이용합니다.
예전 옥션 해킹 사건을 기준으로 예시를 들어보겠습니다.
...
<img src="http://auction.com/changeUserAcoount?id=admin&password=admin" width="0" height="0">
...
- 옥션 관리자가 관리 권한을 가지고(이미 유효한 쿠키 발급된 상태로) 메일을 조회합니다.
- 해커는 위와 같이 태그가 들어간 코드가 담긴 이메일을 보낸다. 단, 관리자는 이미지 크기가 0이므로 이미지가 있는지 눈치를 채지 못합니다.
- 관리자가 메일을 열면 이미지 파일을 받아오기 위해 URL이 열리게 됩니다.
- 그렇게 되면 해커가 원하는 대로 관리자의 계정이 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 |