Spring ModelAndView를 통한 파일 노출

File Disclosure via Spring ModelAndView

설명

Spring MVC의 ModelAndView에 지정한 뷰 이름은 구성된 ViewResolver를 통해 실제 뷰로 해석됩니다. 항상 JSP 파일로 매핑되는 것은 아닙니다. 검증되지 않은 입력이 뷰 이름을 결정하면, 구성에 따라 의도하지 않은 내부 뷰나 리디렉션·포워딩 대상을 선택할 수 있습니다.

잠재적 영향

  • 정보 유출: 접근 권한이 확인되지 않은 뷰에서 민감한 데이터가 노출될 수 있습니다.
  • 의도하지 않은 처리: 선택 가능한 뷰와 리졸버 구성에 따라 내부 처리나 외부 이동이 유발될 수 있습니다. 뷰 이름 제어만으로 임의의 Java 코드가 실행된다고 단정할 수는 없습니다.

해결 방법

  • ModelAndView 사용자 입력을 직접 전달하지 않도록 합니다.
  • 가능할 경우 하드코딩된 안전한 경로만 사용하도록 합니다.
  • Lookup Table을 사용하여 허용된 경로만 접근 가능하도록 제한해야 합니다.
  • 리디렉션이 필요한 경우에도 서버가 정한 목적지만 사용하고 목적지의 접근 권한을 확인하세요. sendRedirect() 자체는 입력 검증을 대신하지 않습니다.

예시

뷰 리졸버와 인증 설정을 생략한 컨트롤러 발췌입니다. home과 profile은 애플리케이션에 정의된 예시 이름이며, 프로필 데이터는 별도로 사용자별 인가를 적용해야 합니다.

변경 전

java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;

@Controller
public class UnsafeController {

    @GetMapping("/view")
    public ModelAndView unsafeView(HttpServletRequest request) {
        String viewName = request.getParameter("page"); // 사용자 입력 사용
        return new ModelAndView(viewName); // 취약한 코드
    }
}

변경 후

java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Controller
public class SafeController {

    private static final Map<String, String> allowedViews = new HashMap<>();

    static {
        allowedViews.put("home", "home");
        allowedViews.put("profile", "profile");
    }

    @GetMapping("/view")
    public ModelAndView safeView(@RequestParam(name = "page", required = false, defaultValue = "home") String page) {
        // Lookup Table를 통한 whitelist 기반 검증
        String safeView = allowedViews.getOrDefault(page, "home");
        return new ModelAndView(safeView);
    }

    @GetMapping("/redirect")
    public void safeRedirect(@RequestParam(name = "page", required = false, defaultValue = "home") String page,
                             HttpServletResponse response) throws IOException {
        String redirectPath = allowedViews.getOrDefault(page, "/home");
        response.sendRedirect(redirectPath); // 안전한 리디렉션
    }
}

설명:

  • 변경 전: page 입력을 ModelAndView의 뷰 이름으로 직접 사용합니다.
  • 변경 후: 고정된 매핑에서 뷰 이름을 선택하고 알 수 없는 값은 home으로 처리합니다. 별도 리디렉션 메서드 역시 고정된 목적지를 사용하며, 상대 URL이 실제 배포 경로에서 올바르게 해석되는지 확인하세요.

관련 CVE

참조