(위키북스) Spring Security in Action
[ ▶ CSRF ]
CSRF (사이트 간 요청 위조)는 광범위한 공격 유형으로 CSRF에 취약한 애플리케이션은 인증 후 사용자가 웹 애플리케이션에서 원치 않는 작업을 실행하게 할 수 있다.
[ ▷ 스프링 시큐리티의 CSRF 보호 작동 방식 ]
업무 환경에서 웹 툴을 이용해 파일을 저장하고 관리하는 시나리오를 예로 들어보면, 이 툴의 웹 인터페이스로 새 파일을 추가하고 레코드의 새 버전을 추가하며 삭제할 수 있다. 어떤 이유로 페이지 하나를 열어보라는 이메일 받는다. 페이지를 열어봤지만 빈 페이지였으며 알려진 웹사이트로 리디렉션된다. 다시 업무를 시작하려고 했지만 모든 파일이 없어진 것을 발견했다.
사용자는 파일을 관리할 수 있는 애플리케이션에 로그인한 상태다. 파일을 추가 변경 또는 삭제할 때 상호 작용하는 웹 페이지는 이러한 작업을 실행하기 위해 서버의 일부 엔드포인트를 호출한다. 이메일의 알 수 없는 링크를 클릭하여 외부 페이지를 열면 이 페이지가 서버를 호출하고 사용자 대신 작업을 실행 (파일 삭제)한다. 이런 일이 가능한 이유는 사용자가 로그인했으므로 서버가 사용자로부터 전달된 작업을 신뢰하기 때문이다.
대다수의 애플리케이션 사용자들은 이러한 보안 위험을 제대로 인식하지 못한다. 따라서 애플리케이션 사용자가 스스로 대비하고 보호하기를 바라며 방치하기 보다는 개발자가 애플리케이션을 보호하는 것이 더 나은 방법이다.
CSRF 공격은 사용자가 웹 애플리키에션에 로그인했다고 가정하며 사용자는 공격자에게 속아서 작업 중인 같은 애플리케이션에서 작업을 실행하는 스크립트가 포함된 페이지를 연다. 사용자가 이미 로그인했기 때문에 위조 코드는 이제 사용자를 가장하고 사용자 대신 작업을 수행할 수 있게 된다.
CSRF 보호는 웹 애플리케이션에서 프런트엔드만 변경 작업 (GET, HEAT, TRACE, OPTIONS, HTTP 방식)을 수행할 수 있게 보장한다. 그러면 외부 페이지가 사용자 대신 작업을 수행할 없게 막을 수 있다.
데이터를 변경하는 작업을 수행하려면 먼저 사용자가 적어도 한번 이상은 HTTP GET으로 웹 페이지를 요청해야 한다. 이때 애플리케이션은 고유한 토크을 생성한다. 이제부터 애플리케이션은 헤더에 이 고유한 값이 들어있는 요청에 대해서만 변경 작업 (GET, PUT, DELETE 등)을 수행한다. 애플리케이션은 토큰의 값을 안다는 것은 다른 시스템이 아닌 애플리케이션 자체가 변경 요청을 보낸 증거라고 본다, POST, PUT, DELETE를 비롯한 변경 호출을 포함하는 모든 페이지는 응답을 통해 CSRF 토큰을 받고 변경 호출을 할 때 이 토큰을 이용해야 한다.
CSRF 보호의 시작점은 필터 체인의 CsrfFilter 필터다. CsrfFilter는 요청을 가로채고 GET, HEAD, TRACE, OPTIONS를 포함하는 HTTP 방식의 요청을 모두 허용하고 다른 모든 요청에는 토큰이 포함된 헤더가 있는지 확인한다. 이 헤더가 없거나 헤더에 잘못된 토큰 값이 포함된 경우 애플리케이션은 요청을 거부하고 응답 상태를 '403 금지됨'으로 설정한다.
토큰은 하나의 문자열 값으로 GET, HEAD, TRACE, OPTIONS 외의 HTTP 방식을 사용할 때 요청의 헤더에 이 토큰을 추가해야 한다. 토큰을 포함하는 헤더를 추가하지 않으면 애플리케이션은 요청을 수락하지 않는다.
CsrfFilter는 CsrfTokenRepository 구성 요소를 이용해 새 토큰 생성, 토큰 저장, 토큰 검증에 필요한 CSRF 토큰 값을 관리한다. 기본적으로 CsrfTokenRepository는 토큰을 HTTP 세션에 저장하고 랜덤 UUID (Universally Unique Identifier)로 토큰을 생성한다. 대부분은 이것으로 충분하지만 구현할 요구 사항이 기본 구현으로 해결되지 않으면 CsrfTokenRepository를 직접 구현하는 방법을 사용해야 한다.
기본적으로 CSRF 보호를 비활성화하지 않으면 POST로 직접 엔드포인트롤 호출할 수 없다. 하지만 CSRF를 비활성화하지 않고 POST 엔드포인트를 호출하는 방법이 있다.
HTTP POST를 호출하려면 헤더에 넣을 CSRF 토큰을 얻어야 한다.
▼ 맟춤형 필터 클래스
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 {
// _csrf 요청 특성에서 토큰 값을 얻어 콘솔에 출력한다.
Object o = request.getAttribute("_csrf");
CsrfToken token = (CsrfToekn) o;
logger.info("CSRF token " + token.getToeke());
filterChain.doFilter(request, response);
}
}
▼ 구성 클래스에 맟춤형 필터 추가
@Configuration
public class AppConfig {
@Bean
SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.addFilterAfter(new CsrfTokenLogger(), CsrfFilter.class)
.authorizeHttpRequests(c -> c.anyRequest().permitAll());
return http.build();
}
}
클라이언트는 CSRF 토큰을 추측하거나 서버 로그에서 읽을 수 없다. 클라이언트가 사용할 HTTP 응답에 토큰 값을 추가할 책임은 백엔드 애플리케이션에 있다.
[ ▷ 실제 시나리오에서 CSFR 보호 사용 ]
CSRF 보호는 브라우저에서 실행되는 웹 애플리케이션에 이용되며, 애플리케이션의 표시된 컨텐츠를 로드하는 브라우저가 변경작업을 수행할 수 있다고 예상될 때 필요하다.
HTTP 방식으로 POST, PUT, DELETE를 이용하는 엔드포인트를 개발하기 위해서는 CSRF 보호가 활성화 상태일 때 CSRF 토큰의 값을 전송하는 작업을 수행해야 한다.
페이지에서 변경 가능한 작업을 호출하는데 이용하는 작업이나 비동기 자바스크립트 요청의 경우 유효한 CSRF 토큰을 보내야 한다. 이는 애플리케이션이 제3자가 보낸 요청을 구분하는 가장 일반적인 방법이다. 제3자의 요청은 사용자가를 가장하려 작업을 실행하려고 할 수 있다.
CSRF 토큰은 같은 서버가 프런트엔드와 백엔드 모두를 담당하는 단순한 아키텍처에서 잘 작동한다. 하지만 클라이언트와 클라이언트가 이용하는 백엔드 솔류션이 독립적일 때는 CSRF 토큰이 잘 작동하지 않는다. 모바일 애플리케이션인 클라이언트가 있거나 독립적으로 개발된 웹 프런트엔드가 있을 때가 이러한 시나리오에 해당한다. 앵귤러, 리액트 또는 Vue.js와 같은 프레임워크로 개발된 웹 클라이언트는 웹 애플리케이션 아키텍처 어디에나 있으므로 이러한 사례에 맞는 보안 접근법을 구현하는 방식도 알아야 한다.
[ ▷ CSRF 보호 맞춤 구성 ]
애플리케이션에는 다양한 요구 사항이 있으므로 프레임워크가 제공하는 모든 구현은 다양한 시나리오에 쉽게 대응할 수 있을 만큼 유연해야 한다. 스프링 시큐리티의 CSRF 보호 메커니즘도 예외가 아니다.
CSRF 보호는 서버에서 생성된 리소스를 이용하는 페이지가 같은 서버에서 생성된 경우에만 이용한다.
기본적으로 CSRF 보호는 GET, HEAD, TRACE, OPTIONS 외의 HTTP 방식으로 호출되는 엔드포인트의 모든 경로에 적용된다. CSRF 보호를 일부 애플리케이션 경로에서만 비활성화는 방법은 아래와 같다.
@Configuration
public class AppConfig {
@Bean
SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.formLogin(c -> c.defaultSuccessUrl("/main", true));
http.csrf(c -> c.ignoringRequestMatchers("/ciao"));
http.addFilterAfter(new CsrfTokenLogger(), CsrfFilter.class)
.authorizeHttpRequests(c -> c.anyRequest().authenticated());
return http.build();
}
애플리케이션에서 CSRF 토큰을 관리하는 방식을 맟춤 구성해야 하는 경우도 많다. 기본적으로 애플리케이션은 서버 쪽의 HTTP 세션에 CSRF 토큰을 저장한다. 이 간단한 접근 방식은 소규모 애플리케이션에 적합하지만 많은 요청을 처리하고 수평적 확장이 필요한 애플리케이션에는 적합하지 않다. HTTP 세션은 상태 저장형이며 애플리케이션의 확장성을 떨어뜨린다.
애플리케이션이 HTTP 세션이 아닌 데이터베이스에 저장하도록 토큰을 관리하는 방법을 변경하려고 한다면, 스프링 시큐리티는 이를 구현하는데 필요한 두 가지 계약을 제공한다.
- CsrfToken: Csrf 토큰 자체를 기술한다.
- CsrfTokenRepository: CSRF 토큰을 생성, 저장, 로드하는 객체를 기술한다.
CsrfToekn 객체에는 계약을 구현할 때 지정해야 하는 세 가지 주요 특징이 있다.
- 요청에서 CSRF 토큰의 값을 포함하는 헤더의 이름 (X-CSRF-TOKEN)
- 토큰의 값을 저장하는 요청의 특성 이름 (_csrf)
- 토큰의 값
일반적으로 CsrfToken 형식의 인스턴스만 이용하면 인스턴스의 특성에 이 세 가지 세부 정보를 저장할 수 있다. 이 기능을 위해 스프링 시큐리티는 DefaultCsrfToken이라는 구현을 제공한다. DefaultCsrfToken은 CsrfToken 계약을 구현하고 필요한 값 (요청 특성과 헤더의 이름, 토큰의 값)을 포함하는 변경이 불필요한 인스턴스를 만든다.
CsrfTokenRepository는 스프링 시큐리티에서 CSRF 토큰을 관리하는 책임을 맡는다. CsrfTokenRepository 인터페이스는 CSRF 토큰을 관리하는 구성 요소를 나타내는 계약이다. 애플리케이션이 토큰을 관리하는 방법을 변경하려면 CsrfTokenRepository 인터페이스를 구현해 맞춤형 구현을 프레임워크에 연결해야 한다.
데이터베이스의 테이블에 CSRF 토큰을 저장하고 클라이언트에 이 토큰을 식별하기 위한 ID가 있다고 가정한다, 애플리케이션이 CSRF 토큰을 얻고 검증하려면 이 ID가 필요하다. 일반적으로 이 고유 ID는 로그인 중에 얻으며 사용자가 로그인할 때마다 달라야 한다. 토큰을 관리하는 이 전략은 토큰을 메모리에 저장하는 것과 비슷하면 이 경우엔 세션 ID를 이용한다.
대안으로 수명이 정의된 CSRF 토큰을 사용하는 방법이 있다. 이 방식에서 토큰은 정의한 시간이 지나면 만료된다. 토큰을 특정 사용자 ID와 연결하지 않고 데이터베이스에 저장할 수 있다. 요청을 허용할지 말지 결정하려면 HTTP 요청을 통해 제공된 토큰이 존재하는지, 그리고 만료되지 않았는지를 확인하면 된다.
특정 클라이언트를 위해 데이터베이스에서 CSRF 토큰을 얻는 메서드는 findTokenByIdentifier()가 유일하다.
public interface JpaTokenRepository
extends JpaRepository<Token, Integer> {
Optional<Token> findTokenByIdentifier(String indentifier);
}
CRSF 보호 메커니즘은 애플리케이션이 새 토큰을 생성해야할 때 generateToken() 메서드를 호출한다. UUID 클래스를 이용해 새로운 임의의 UUID 값을 생성하고 스프링 시큐리티의 기본 구현과 같이 요청 헤더와 특성의 이름을 X-CSRF-TOKEN 및 _csrf로 유지한다.
@Override
public CsrfToken generateToken(HttpServletRequest httpServletRequest) {
String uuid = UUID.randomUUID().toString();
return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", uuid);
}
CSRF 토큰의 값을 얻으려면 HTTP GET 엔드포인트를 호출한다. 호출할 때는 요구 사항에서 가정한 대로 X-INDENTIFIER 헤더 안에 클라이언트의 ID를 넣어야 한다. 그러면 새로운 CSRF 토큰 값이 생성되고 데이터베이스에 저장된다.
[ ▷ 정리 ]
- CSRF (사이트 간 요청 위조)는 사용자를 속여 위조 스크립트가 포함된 페이지에 접근하도록 하는 공격 유형이다. 이 스크립트는 애플리케이션에 로그인한 사용자를 가장해 사용자 대신 작업을 수행한다.
- CSRF 보호는 스프링 시큐리티에서 기본적으로 활성화된다.
- 스프링 시큐리티 아키텍처에서 CSFR 보호 논리의 진입점은 HTTP 필터다.
'Spring Boot > Spring Security' 카테고리의 다른 글
OAuth2와 OpenID Connect (4) | 2024.12.18 |
---|---|
CORS (Cross Origin Resource Sharing) (0) | 2024.11.01 |
Jason Web Token (0) | 2024.11.01 |