1. 소개
Spring Framework 버전 5.0 ~ 5.0.4, 4.3 ~ 4.3.14 및 기타 이전 버전에는 Windows 시스템에서 디렉터리 또는 경로 탐색 Security 취약점이 있었습니다.
정적 리소스를 잘못 구성하면 악의적인 사용자가 서버의 파일 시스템에 액세스할 수 있습니다. 예를 들어 file: protocol을 사용하여 정적 리소스를 제공하면 Windows에서 파일 시스템에 대한 잘못된 액세스를 제공합니다 .
Spring Framework는 취약점 을 인지하고 이후 릴리스에서 수정했습니다.
따라서 이 수정 사항은 경로 탐색 공격으로부터 애플리케이션을 보호합니다. 그러나 이 수정으로 인해 이전 URL 중 일부는 이제 org.springframework.security.web.firewall.RequestRejectedException 예외 를 발생 시킵니다.
마지막으로 이 예제에서는 경로 탐색 공격의 맥락에서 org.springframework.security.web.firewall.RequestRejectedException 및 StrictHttpFirewall 에 대해 알아보겠습니다 .
2. 경로 순회 취약점
경로 순회 또는 디렉토리 순회 취약점은 웹 문서 루트 디렉토리 외부에서 불법적인 액세스를 가능하게 합니다. 예를 들어 URL을 조작하면 문서 루트 외부의 파일에 무단으로 액세스할 수 있습니다.
가장 최신의 인기 있는 웹 서버가 이러한 공격의 대부분을 상쇄하지만 공격자는 여전히 "./", "../"와 같은 특수 문자의 URL 인코딩을 사용하여 웹 서버 Security을 우회하고 불법적인 액세스 권한을 얻을 수 있습니다.
또한 OWASP 에서는 Path Traversal 취약점과 이를 해결하는 방법에 대해 설명합니다.
3. 스프링 프레임워크 취약점
이제 이 취약점을 수정하는 방법을 배우기 전에 이 취약점을 복제해 보겠습니다.
먼저 Spring Framework MVC 예제를 복제해 보겠습니다. 나중에 pom.xml 을 수정하여 기존 Spring Framework 버전을 취약한 버전으로 교체해 보겠습니다.
리포지토리를 복제합니다.
git clone git@github.com:spring-projects/spring-mvc-showcase.git
복제된 디렉터리 내에서 5.0.0.RELEASE 를 Spring Framework 버전으로 포함 하도록 pom.xml 을 편집합니다.
<org.springframework-version>5.0.0.RELEASE</org.springframework-version>
다음으로 웹 구성 클래스 WebMvcConfig 를 편집하고 addResourceHandlers 메서드를 수정 하여 파일 을 사용하여 리소스를 로컬 파일 디렉터리에 매핑 합니다.
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("/resources/**")
.addResourceLocations("file:./src/", "/resources/");
}
나중에 아티팩트를 빌드하고 웹 앱을 실행합니다.
mvn jetty:run
이제 서버가 시작되면 URL을 호출합니다.
curl 'http://localhost:8080/spring-mvc-showcase/resources/%255c%255c%252e%252e%255c/%252e%252e%255c/%252e%252e%255c/%252e%252e%255c/%252e%252e%255c/windows/system.ini'
%252e%252e%255c 는 ..\의 이중 인코딩 형식이고 %255c%255c 는 \\의 이중 인코딩 형식입니다.
위태롭게도 응답은 Windows 시스템 파일 system.ini의 내용이 될 것입니다.
4. 스프링 시큐리티 HttpFirewall 인터페이스
Servlet 사양 은 servletPath 와 pathInfo 간의 구분을 정확하게 정의하지 않습니다 . 따라서 이러한 값의 변환에서 서블릿 컨테이너 간에 불일치가 있습니다.
예를 들어, Tomcat 9 에서 URL http://localhost:8080/api/v1/users/1 의 경우 URI /1 은 경로 변수로 사용됩니다.
반면 다음은 /api/v1/users/1 을 반환합니다 .
request.getServletPath()
그러나 아래 명령은 null 을 반환합니다 .
request.getPathInfo()
URI에서 경로 변수를 구분할 수 없으면 경로 탐색/디렉토리 탐색 공격과 같은 잠재적인 공격이 발생할 수 있습니다. 예를 들어, 사용자는 \\, /../, . .\ URL. 불행히도 일부 Servlet 컨테이너만 이러한 URL을 정규화합니다.
구출에 스프링 시큐리티 . Spring Security는 컨테이너 전체에서 일관되게 작동하며 HttpFirewall 인터페이스를 사용하여 이러한 종류의 악성 URL을 정규화합니다. 이 인터페이스에는 두 가지 구현이 있습니다.
4.1. 기본Http방화벽
우선 구현 클래스의 이름과 혼동하지 말자. 즉, 이것은 기본 HttpFirewall 구현이 아닙니다.
방화벽은 URL을 삭제하거나 정규화하려고 시도 하고 컨테이너 전체에서 servletPath 및 pathInfo 를 표준화합니다 . 또한 명시적으로 @Bean 을 선언 하여 기본 HttpFirewall 동작을 재정의할 수 있습니다 .
@Bean
public HttpFirewall getHttpFirewall() {
return new DefaultHttpFirewall();
}
그러나 StrictHttpFirewall 은 강력하고 안전한 구현을 제공하며 권장되는 구현입니다.
4.2. StrictHttp방화벽
StrictHttpFirewall 은 HttpFirewall의 더 엄격한 기본 구현입니다 . 대조적으로 DefaultHttpFirewall 과 달리 StrictHttpFirewall 은 보다 엄격한 보호를 제공하는 비정규화된 URL을 거부합니다. 또한 이 구현은 XST(Cross-Site Tracing) 및 HTTP 동사 변조 와 같은 여러 다른 공격으로부터 애플리케이션을 보호합니다 .
또한 이 구현은 사용자 정의가 가능하며 합리적인 기본값이 있습니다. 즉, URI의 일부로 세미콜론을 허용하는 것과 같은 몇 가지 기능을 비활성화(권장하지 않음)할 수 있습니다.
@Bean
public HttpFirewall getHttpFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall.setAllowSemicolon(true);
return strictHttpFirewall;
}
간단히 말해서 StrictHttpFirewall 은 org.springframework.security.web.firewall.RequestRejectedException 으로 의심스러운 요청을 거부합니다 .
마지막으로 Spring REST 및 Spring Security 를 사용하여 사용자에 대한 CRUD 작업으로 사용자 관리 애플리케이션을 개발하고 StrictHttpFirewall 이 작동하는 모습을 살펴 보겠습니다 .
5. 의존성
Spring Security 와 Spring 웹 의존성 을 선언하자 :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.4</version>
</dependency>
6. 스프링 Security 설정
다음으로 SecurityFilterChain 빈 을 생성하는 구성 클래스를 생성하여 기본 인증으로 애플리케이션을 보호해 보겠습니다 .
@Configuration
public class HttpFirewallConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
.antMatchers("/error")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic();
return http.build();
}
}
기본적으로 Spring Security는 다시 시작할 때마다 변경되는 기본 비밀번호를 제공합니다. 따라서 application.properties 에 기본 사용자 이름과 비밀번호를 생성해 보겠습니다 .
spring.security.user.name=user
spring.security.user.password=password
이제부터는 이러한 자격 증명을 사용하여 Security REST API에 액세스합니다.
7. Security REST API 구축
이제 사용자 관리 REST API를 빌드해 보겠습니다.
@PostMapping
public ResponseEntity<Response> createUser(@RequestBody User user) {
userService.saveUser(user);
Response response = new Response()
.withTimestamp(System.currentTimeMillis())
.withCode(HttpStatus.CREATED.value())
.withMessage("User created successfully");
URI location = URI.create("/users/" + user.getId());
return ResponseEntity.created(location).body(response);
}
@DeleteMapping("/{userId}")
public ResponseEntity<Response> deleteUser(@PathVariable("userId") String userId) {
userService.deleteUser(userId);
return ResponseEntity.ok(new Response(200,
"The user has been deleted successfully", System.currentTimeMillis()));
}
이제 애플리케이션을 빌드하고 실행해 보겠습니다.
mvn spring-boot:run
8. API 테스트
이제 cURL 을 사용하여 사용자 를 생성하여 시작하겠습니다 .
curl -i --user user:password -d @request.json -H "Content-Type: application/json"
-H "Accept: application/json" http://localhost:8080/api/v1/users
다음은 request.json 입니다 .
{
"id":"1",
"username":"navuluri",
"email":"bhaskara.navuluri@mail.com"
}
결과적으로 응답은 다음과 같습니다.
HTTP/1.1 201
Location: /users/1
Content-Type: application/json
{
"code":201,
"message":"User created successfully",
"timestamp":1632808055618
}
이제 모든 HTTP 메서드의 요청을 거부하도록 StrictHttpFirewall 을 구성해 보겠습니다.
@Bean
public HttpFirewall configureFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall
.setAllowedHttpMethods(Collections.emptyList());
return strictHttpFirewall;
}
다음으로 API를 다시 호출해 보겠습니다. 모든 HTTP 메서드를 제한하도록 StrictHttpFirewall 을 구성 했으므로 이번에는 오류가 발생합니다.
로그에는 다음과 같은 예외가 있습니다.
org.springframework.security.web.firewall.RequestRejectedException:
The request was rejected because the HTTP method "POST" was not included
within the list of allowed HTTP methods []
Spring Security v5.4 부터 RequestRejectedException 이 있을 때 RequestRejectedHandler 를 사용 하여 HTTP 상태 를 사용자 정의할 수 있습니다 .
@Bean
public RequestRejectedHandler requestRejectedHandler() {
return new HttpStatusRequestRejectedHandler();
}
HttpStatusRequestRejectedHandler 를 사용할 때 기본 HTTP 상태 코드 는 400 입니다. 그러나 HttpStatusRequestRejectedHandler 클래스의 생성자에 상태 코드를 전달하여 이를 사용자 지정할 수 있습니다.
이제 URL 및 HTTP GET , POST , DELETE 및 OPTIONS 메서드 에서 \\ 를 허용하도록 StrictHttpFirewall 을 재구성해 보겠습니다.
strictHttpFirewall.setAllowBackSlash(true);
strictHttpFirewall.setAllowedHttpMethods(Arrays.asList("GET","POST","DELETE", "OPTIONS")
다음으로 API를 호출합니다.
curl -i --user user:password -d @request.json -H "Content-Type: application/json"
-H "Accept: application/json" http://localhost:8080/api<strong>\\</strong>v1/users
그리고 여기에 응답이 있습니다.
{
"code":201,
"message":"User created successfully",
"timestamp":1632812660569
}
마지막으로 @Bean 선언 을 삭제하여 StrictHttpFirewall 의 원래 엄격한 기능으로 되돌리자.
다음으로 의심스러운 URL로 API를 호출해 보겠습니다.
curl -i --user user:password -d @request.json -H "Content-Type: application/json"
-H "Accept: application/json" http://localhost:8080/api/v1<strong>//</strong>users
curl -i --user user:password -d @request.json -H "Content-Type: application/json"
-H "Accept: application/json" http://localhost:8080/api/v1<strong>\\</strong>users
바로 위의 모든 요청이 오류 로그와 함께 실패합니다.
org.springframework.security.web.firewall.RequestRejectedException:
The request was rejected because the URL contained a potentially malicious String "//"
9. 결론
이 문서에서는 경로 탐색/디렉터리 탐색 공격을 일으킬 수 있는 악성 URL에 대한 Spring Security의 보호에 대해 설명합니다.
DefaultHttpFirewall 은 악성 URL을 정규화하려고 합니다. 그러나 StrictHttpFirewall 은 RequestRejectedException 으로 요청을 거부합니다 . Path Traversal 공격과 함께 StrictHttpFirewall 은 다른 여러 공격으로부터 우리를 보호합니다. 따라서 기본 구성과 함께 StrictHttpFirewall 을 사용하는 것이 좋습니다 .
항상 그렇듯이 전체 소스 코드는 Github에서 사용할 수 있습니다 .