크로스 사이트 요청 위조
크로스 사이트 요청 위조(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 |