10장 CSRF 보호와 CORS 적용
이 블로그 내용은 아래 도서를 학습한 내용입니다.
학습 도서 정보
스프링 시큐리티 인 액션(Spring Security in Action)
로렌티우 스필카 지음
최민석 옮김
위키북스
- CSRF 보호 구현
- CSRF 보호 맞춤 구성
- CORS 구성 적용
스프링 시큐리티에는 CORF 보호를 적용하는 필터와 CORS 구성과 관련된 필터처럼 필터 체인에 추가하는 자체 필터도 있다.
10. 1 애플리케이션에 CSRF (사이트 간 요청 위조) 보호 적용
CSRF는 광범위한 공격 유형이며 CSRF에 취약한 애플리케이션은 인증 후 사용자가 웹 애플리케이션에서 원치 않는 작업을 실행하게 할 수 있다.
- CSRF가 무엇이고 어떻게 작동하는지 검토
- CSRF 취약성을 완화하는데 이용되는 CSRF 토큰 메커니즘에 대해 검토
- HTTP POST 방식으로 엔드포인트를 호출하는 방법을 학습
- REST 엔드포인트를 이용하는 작은 애플리케이션 학습
- CSRF 토큰 메커니즘을 맞춤 구성하는 방법을 학습
10. 1. 1 스프링 시큐리티의 CSRF 보호가 작동하는 방식
- CSRF 공격은 사용자가 웹 애플리케이션에 로그인했다고 가정하며 사용자는 공격자에게 속아서 작업중인 같은 애플리케이션에서 작업을 실행하는 스크립트가 포함된 페이지를 연다.
- 사용자가 이미 로그인했기 때문에 위조 코드는 이제 사용자를 가장하고 사용자 대신 작업을 수행할 수 있다.
CSRF 공격 예시
- CSRF 보호는 웹 애플리케이션에서 프런트 엔드만 변경 작업을 수행할 수 있게 보장한다.(관례상 GET, HEAD, TRACE, OPTIONS 외의 HTTP 방식)
- 데이터를 변경하는 작업을 수행하려면 먼저 사용자가 적어도 한 번은 HTTP GET으로 웹 페이지를 요청해야 한다.
- 이때 애플리케이션은 고유한 토큰을 생성한다. 다음부터의 변경작업 시 토큰을 헤더에 넣어서 요청한다. (POST, PUT, DELETE 등)
- 애플리케이션은 토큰의 값을 안다는 것은 다른 시스템이 아닌 애플리케이션 자체가 변경 요청을 보낸 증거라고 본다.
CsrfFilter
CSRF 보호의 시작점은 필터 체인의 CsrfFilter라는 한 필터다. CsrfFilter는 요청을 가로채고 GET, HEAD, TRACE, OPTIONS를 포함하는 HTTP 방식의 요청을 모두 허용하고 다른 모든 요청에는 토큰이 포함된 헤더가 있는지 확인한다. 이 헤더가 없거나 헤더에 잘못된 토큰 값이 포함된 경우 애플리케이션은 요청을 거부하고 응답의 상태를 '403 금지됨'으로 설정한다.
CsrfFilter는 CsrfTokenRepository 구성 요소를 이용해 새 토큰 생성, 토큰 저장, 토큰 검증에 필요한 CSRF 토큰 값을 관리한다. 기본적으로 CsrfTokenRepository는 토큰을 HTTP 세션에 저장하고 랜덤 UUID로 토큰을 생성한다. 대부분은 이것으로 충분하지만 구현할 요구 사항이 기본 구현으로 해결되지 않으면 CsrfTokenRepository를 직접 구현하는 방법이 있다. (=> 10.3절)
Csrf 토큰을 로그로 출력하는 예제
- CsrfFilter는 생성된 CSRF 토큰을 HTTP 요청의 _csrf 특성에 추가한다.
@RestController
public class HelloController {
@GetMapping("/hello")
public String getHello() {
return "Get Hello!";
}
@PostMapping("/hello")
public String postHello() {
return "Post Hello!";
}
}
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);
}
}
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAfter(
new CsrfTokenLogger(),
CsrfFilter.class)
.authorizeRequests()
.anyRequest().permitAll();
}
}
1. csrf 토큰을 로그로 출력하기 위해 GET /hello 호출
ㄴ 요청
❯ curl http://localhost:8080/hello
ㄴ 응답
...
< Set-Cookie: JSESSIONID=07EE50153547CF0E00851FE6345BDA5D; Path=/; HttpOnly
...
2. 로그
CSRF token b889dd4f-c8cf-4c14-a11a-f30f265363cf
3. 토큰을 적용하지 않는 POST 요청 -> 403
❯ curl -XPOST http://localhost:8080/hello
{
"timestamp":"2023-10-12T17:11:49.743+00:00",
"status":403,
"error":"Forbidden",
"message":"",
"path":"/hello"
}
4. CSRF 토큰을 지정하여 POST 요청
(CSRF 토큰 값은 세션에 저장하므로 세션ID(JSESSIONID)도 지정해주어야 한다.)
❯ curl -XPOST 'http://localhost:8080/hello' \
--header 'Cookie: JSESSIONID=F773B303144579215D9A7B38E23FA1E1; JSESSIONID=F773B303144579215D9A7B38E23FA1E1' \
--header 'X-CSRF-TOKEN: 51f6ba91-b200-42a4-8d39-fc6083080611'
Post Hello!%
이 예제에서는 GET /hello 를 요청했을때 CSRF 토큰을 얻을 수 없다. 아직 HTTP 응답에 CSRF 토큰 값을 추가하지 않았기 때문이다. 클라이언트가 사용할 HTTP 응답에 CSRF 토큰 값을 추가할 책임은 백엔드 애플리케이션에 있다.
10. 1. 2 실제 시나리오에서 CSRF 보호 사용
CSRF 보호는 브라우저에서 실행되는 웹 앱에 이용되며, 앱의 표시된 콘텐츠를 로드하는 브라우저가 변경 작업을 수행할 수 있다고 예상될 때 필요하다.
확인할 예
- 로그인 양식을 이용해 웹 애플리케이션의 예제 구축
- 로그인의 기본 구현이 CSRF 토큰을 이용하는 방법 확인
- 주 페이지에서 HTTP POST 호출 구현
10. 1. 3 CSRF 보호 맞춤 구성
10. 2 CORS(교차 출처 리소스 공유) 이용
10. 2. 1 CORS 작동 방식
10. 2. 2 @CrossOrigin 어노테이션으로 CORS 정책 적용
10. 2. 3 CorsConfigurer로 CORS 적용
요약