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.RequestRejectedExceptionStrictHttpFirewall 에 대해 알아보겠습니다 .

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 사양servletPathpathInfo 간의 구분을 정확하게 정의하지 않습니다 . 따라서 이러한 값의 변환에서 서블릿 컨테이너 간에 불일치가 있습니다.

예를 들어, 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을 삭제하거나 정규화하려고 시도 하고 컨테이너 전체에서 servletPathpathInfo 를 표준화합니다 . 또한 명시적으로 @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 RESTSpring Security 를 ​​사용하여 사용자에 대한 CRUD 작업으로 사용자 관리 애플리케이션을 개발하고 StrictHttpFirewall 이 작동하는 모습을 살펴 보겠습니다 .

5. 의존성

Spring SecuritySpring 웹 의존성 을 선언하자 :

<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 , DELETEOPTIONS 메서드 에서 \\ 를 허용하도록 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에서 사용할 수 있습니다 .

Security footer banner