헬로우월드 2025. 2. 27. 23:57

Spring MVC는 요청을 적절한 핸들러(컨트롤러)로 라우팅하기 위해 경로(Path) 매칭을 수행합니다.
그러나, 경로 매칭 과정에서 Servlet API의 경로 처리 방식과 URL 디코딩 문제 등으로 인해 다양한 복잡성이 발생할 수 있습니다.
이를 해결하기 위해 Spring MVC는 여러 가지 전략을 제공합니다.

 

1. Servlet API가 제공하는 경로 정보

Servlet API는 요청 경로를 여러 부분으로 나누어 제공합니다.

경로 유형 설명 예제 (/myapp/api/users/123)
requestURI 전체 요청 경로 /myapp/api/users/123
contextPath 애플리케이션 컨텍스트 경로 (기본적으로 web.xml에서 설정) /myapp
servletPath 서블릿 매핑 경로 /api
pathInfo 추가적인 경로 정보 /users/123

📌 즉, Spring MVC는 요청을 핸들러에 매핑할 때 contextPathservletPath를 제외한 lookupPath를 계산해야 합니다.

 

2. Path Decoding 문제와 보안 이슈

경로를 비교하기 위해 requestURI에서 contextPath 및 servletPath를 제외한 lookupPath를 추출해야 합니다.
그러나 경로를 디코딩할 때 몇 가지 문제가 발생할 수 있습니다.

🚨 문제 1: 디코딩된 경로 비교 불가능

  • servletPathpathInfo는 디코딩된 상태에서 제공되지만, requestURI는 인코딩된 상태로 존재합니다.
  • 따라서 requestURI를 디코딩해야만 비교할 수 있습니다.
  • 하지만 이 과정에서 예약된 문자(예: /, ;)가 디코딩되어 경로 구조가 변경될 위험이 있습니다.

📌 예제:

  • 원본 요청: /api%2Fusers/123 (인코딩된 /api/users/123):일반적으로 url에서 / 의 용도는 Separator(구분 기호)입니다. 그러나 이 예제에서 api와users 사이에 url의 Separator / 가 아닌 문자 /를 사용할 때 /가 %2F로 인코딩됩니다.
  • 디코딩 후: /api/users/123
  • / 문자가 디코딩되면서 경로가 달라져 보안 문제가 발생할 수 있음

🚨 문제 2: Servlet 컨테이너마다 servletPath 처리 방식이 다름

  • 일부 서블릿 컨테이너는 servletPath를 정규화(Normalize)하여 제공하지만, 그렇지 않은 경우도 있음
  • 결과적으로 requestURI.startsWith(servletPath) 비교가 불가능한 경우가 있음

📌 결론:
servletPath를 직접 사용하는 것이 비추천되며, DispatcherServlet이 servletPath 없이 동작하는 방식이 더 바람직함.

 

3. DispatcherServlet이 "/" 매핑을 사용할 경우

만약 DispatcherServlet이 / 또는 /*로 매핑되어 있다면,
Spring MVC는 서블릿 4.0+ 환경에서 servletPath 및 pathInfo를 완전히 무시할 수 있습니다.

 

📌 즉, 서블릿 컨테이너가 4.0 이상이면, Spring MVC가 servletPath와 pathInfo를 신경 쓰지 않고 requestURI만으로 처리 가능!

서블릿 3.1 환경에서는 어떻게 해결?

  • UrlPathHelper를 사용하여 alwaysUseFullPath=true 설정하면 같은 효과를 얻을 수 있음
@Bean
public UrlPathHelper urlPathHelper() {
    UrlPathHelper helper = new UrlPathHelper();
    helper.setAlwaysUseFullPath(true);
    return helper;
}

 

4. Path Decoding의 보안 문제 해결

경로를 매칭할 때 디코딩된 경로를 비교해야 하지만, 디코딩 시 보안 문제가 발생할 수 있습니다.

🚨 해결 방법 1: Spring Security의 HTTP Firewall을 사용하여 예약된 문자 차단

  • Spring Security를 활용하면 / 등의 예약된 문자가 포함된 요청을 거부할 수 있음
@Bean
public HttpFirewall httpFirewall() {
    DefaultHttpFirewall firewall = new DefaultHttpFirewall();
    return firewall;
}

 

🚨 해결 방법 2: UrlPathHelper에서 URL 디코딩 비활성화

  • 디코딩된 경로 비교를 하지 않고, 인코딩된 상태 그대로 비교할 수도 있음
@Bean
public UrlPathHelper urlPathHelper() {
    UrlPathHelper helper = new UrlPathHelper();
    helper.setUrlDecode(false); // URL 디코딩 비활성화
    return helper;
}

📌 주의:
컨트롤러의 요청 매핑도 인코딩된 상태로 작성해야 할 수도 있음.

 

5. PathPatternParser 사용 (Spring MVC 5.3+)

Spring MVC 5.3부터는 기존의 AntPathMatcher 대신 PathPatternParser를 사용할 수 있습니다.
Spring MVC 6.0부터는 기본적으로 PathPatternParser가 활성화됩니다.

📌 기존 방식: AntPathMatcher

  • lookupPath를 디코딩하거나 컨트롤러 매핑을 인코딩해야 경로 비교 가능

📌 새로운 방식: PathPatternParser

  • 경로를 RequestPath 객체로 변환 후 비교
  • 경로를 한 개의 세그먼트(segment) 단위로 개별 디코딩하여 비교
  • 보안 이슈 없이 경로 구조 유지 가능

✅ PathPatternParser 사용 방법

@Bean
public WebMvcConfigurer configurer() {
    return new WebMvcConfigurer() {
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            configurer.setPatternParser(new PathPatternParser());
        }
    };
}

📌 이점:

  • 보안 문제 해결: "/api%2Fusers/123"을 안전하게 매칭 가능
  • URL 매칭 정확성 증가: lookupPath 디코딩 불필요
  • 서블릿 매핑(prefix mapping) 지원: 서블릿 경로가 포함된 URL도 안전하게 처리 가능

🚀 Summary

1️⃣ Servlet API의 경로 처리 방식

  • requestURI, contextPath, servletPath, pathInfo로 나누어져 있음
  • Spring MVC는 lookupPath를 생성해야 함

2️⃣ 경로 비교 시 발생하는 문제점

  • servletPath가 컨테이너마다 다르게 정규화됨
  • requestURI를 디코딩할 때 보안 문제가 발생할 수 있음

3️⃣ 해결 방법

  • DispatcherServlet을 /로 매핑하면 servletPath를 사용하지 않도록 할 수 있음
  • Spring Security HTTP Firewall을 이용해 보안 강화
  • UrlPathHelper에서 URL 디코딩을 비활성화 가능 (urlDecode=false)

4️⃣ Spring MVC 5.3+의 PathPatternParser 사용

  • 기존의 AntPathMatcher보다 안전하고 정확한 경로 매칭
  • 경로 세그먼트 단위로 개별 디코딩하여 보안 문제 해결
  • Spring MVC 6.0부터 기본 활성화

 

📌 결론:
Spring MVC 6.0 이상에서는 PathPatternParser를 사용하는 것이 가장 안전하고 추천되는 방식입니다! 🚀

 

출처 : https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/handlermapping-path.html