Hello

Tomcat Early Error(400/501/505등) 기본 에러페이지 노출 차단을 위한 대응 본문

카테고리 없음

Tomcat Early Error(400/501/505등) 기본 에러페이지 노출 차단을 위한 대응

nari0_0 2026. 1. 16. 11:03
728x90

배경

ISMS 심사 과정에서 Tomcat 기본 에러 페이지 노출이 보안 이슈로 지적되었다.

  •  대상 에러 예시 : 400, 505, 501
  •  문제 특징 : 위 에러들은 서블릿/어플리케이션 진입 전 (HTTP 요청 파싱 단계) 에서 발생한다.
  •  문제 : 어플리케이션에 설정한 에러페이지로 처리되지 않고 Tomcat 기본 에러 페이지가 그대로 내려감

생각한 해결 방향

서블릿 이전 단계(early error)에서 발생하는 에러도 서비스 정책에 맞는 커스텀 HTML로 내려서, 톰캣 기본 에러페이지 노출 되지 않도록 생각했다.

Valve란(왜 Valve를 봤는가)

Tomcat의 컨테이너 파이프라인(Container → Host → Context) 에 끼워 넣는 컴포넌트로 서블릿 필터보다 앞 톰캣 내부에서 동작한다.

  • 예: AccessLogValve, RemoteIpValve, ErrorReportValve 
  • 요청이 어플리케이션에 들어가기 전/후 에러 응답 생성에 관여할 수 있엇 early error 처리에 적합하다고 생각했다. 

시도 내역

실행 1) Tomcat 8.5 + Custom ErrorReportValve 구현 + server.xml에 <Valve>로 추가 (실패)

  • ErrorReportValve를 상속한 커스텀 밸브를 구현
  • JAR로 패키징 후 $TOMCAT_HOME/lib에 배포
  • server.xml의 <Host> 하위에 다음과 같이 Valve 추가
<Host ...>
  <Valve className="com.nari.CustomValve"/>
</Host>

결과

  • 컨테이너 내부에서 발생한 일부 에러 CustomValve로 처리됨
  • 400, 505 같은 early error는 ErrorReportValve로 전달되어 원하는 분기 처리가 되지 않음
  • 결론 : Valve는 적용되었지만 목표로 했던 서빌릿 진입 전 에러는 해결되지 않음

실행 2) Tomcat 8.5 + errorCode 옵션 (실패)

Tomcat 공식 문서상 ErrorReportValve에는 상태코드별 에러 페이지를 지정하는 설정(errorCode.*)이 존재한다. 8.5, 9 버전 모두 존재하는 옵션이다.

        <Valve className="org.apache.catalina.valves.ErrorReportValve"
               showReport="false"
               showServerInfo="false"
               errorCode.0="E:\apache-tomcat-8.5.42/error.html" />

 

  • 동일하게 Tomcat 기본 ErrorReportValve HTML이 출력됨
  • 즉, 8.5에서는 기대한 “errorCode 기반 커스텀 페이지 렌더링”이 early error에 적용되지 않았다.

실행 3) tomcat 버전 9 업그레이드 + errorCode (성공)

  • Tomcat 8.5 → Tomcat 9 업그레이드
  • 시도 2에서 사용한 동일한 ErrorReportValve 설정 적용

chatgpt와 추측으로는 최소한의 request, context가 생성된 뒤 ErrorReportValve에서 errorCode 매핑을 확인하도록 내부 흐름을 개선한것으로 추측한다.

 

+) 8.5와 9 버전에서 errorCode 설정 다르게 동작

early error가 ErrorReportValve의 “매핑 적용 경로”까지 도달 가능한지(흐름/진입 조건)가 버전별로 달라진 것 같다.

  • 400/501/505 등 커넥터 단계에서 조기 발생 가능한 에러는 서블릿 컨테이너 진입 전에 응답이 만들어 질 수 있다.
  • Tomcat 8.5에서는 early error 처리 시 errorCode 매핑을 적용하기 어려운 흐름이 존재 하는 것으로 예상한다.
    • ErrorReportValve의 errorCode.* 매핑 로직까지 도달하지 못하거나, 해당 속성 자체가 정상 인식되지 않아(경고 로그) 기본 페이지 렌더링으로 빠질 가능성이 있음
    • 결과적으로 errorCode.*가 적용되지 않고 기본 페이지가 내려옴
  • Tomcat 9에서는 early error에서도 errorCode 매핑을 적용할 수 있도록 흐름이 개선된 것으로 보인다.
    • 동일 설정으로 커스텀 HTML이 정상 적용됨

++) 옴팡진 테스트로 알게된 사실

Tomcat 8.5.42에서는 errorCode.nnn 설정이 Digester 단계에서 “프로퍼티 미인식”으로 무시된다

server.xml에 errorCode.0작성 시 아래 경고 메시지가 발생했다.

Tomcat 설정 파일을 읽는 과정에서 잘못된 속성 설정이 발견되었다는 의미입니다.

server.xml일부
<Valve className="org.apache.catalina.valves.ErrorReportValve"
               errorCode.0="E:/apache-tomcat-8.5.42/conf/error1.html"
               showReport="false"/>
--------------------------------------
실행 로그
16-Jan-2026 17:32:48.711 경고 [main] org.apache.tomcat.util.digester.SetPropertiesRule.begin [SetPropertiesRule]{Server/Service/Engine/Host/Valve} Setting property 'errorCode.0' to 'E:/somepath/error1.html' did not find a matching property.

 

내가 확인한 8.5.100 문서에는 errorCode.nnn 옵션이 공식적으로 존재한다.

동일 설정을 Tomcat 8.5.100으로 올려 적용했을 때 정상 동작(error1.html)을 확인했다.

내 테스트 결과로는 8.5.42 는 적용되지 않고 경고가 떳고 8.5.100이상 부터 정상 동작 하는 것을 확인했다.

 

챗지피티 피셜)
Tomcat 8.5 계열 전반에서 errorCode.nnn 는 공식적으로 지원되는 속성입니다.
✖️ 하지만 초기 8.5.x 버전(예: 8.5.42 등)에서는 Digester가 이 속성을 정상적으로 인식하지 못해 워닝을 발생시키는 경우가 있었을 수 있습니다.
✔️ 8.5.75 → 8.5.100 등 최신 8.5.x에서는 워닝 없이 제대로 적용되는 사례가 많습니다.

 

+) 추후 알게된 내용!!( <hsot errorReportValveClas="customValve" 로 ErrorReportValve 자체 교체)

이번 대응은 Tomcat 9 + Valve errorCode 옵션으로 문제를 해결 했는데 좀 더 알아보니 errorReportValve 자체를 바꾸는 방법도 있었다. 

 

외장 Tomcat에서 교체 방법 (8.5, 9동일한 결과)

  • server.xml의 <host>에 errorReportValveClass 속성을 지정해 교체할 수 있다.
  • 커스텀 클래스는 jar로 만들어 $CATALINA_HOME/lib에 배포해야 한다.
  • 이 방식은 valve를 추가하는 것이 아니라 에러 리포트 벨브 자체를 교체하는 방법이다.
    <Host name="localhost" appBase="webapps"
                  unpackWARs="true" autoDeploy="true"
                  errorReportValveClass="com.nari.CustomValve">

 

내장 Tomcat에서 교체방법 (spring boot  + 9버전)

  • 이 코드는 embedded Tomcat을 구성하는 과정에서 StandardHost에 접근해 setErrorReportValveClass()로 에러 리포트 벨브를 교체하는 방법이다.
  • 외장 톰캣의 <Host errorReportValveclass="~">와 동일한 개념이다. 
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> webServerFactoryCustomizer() {
    return factory -> factory.addContextCustomizers(context -> {
        Container parent = context.getParent();
        if (parent instanceof StandardHost host) {
            host.setErrorReportValveClass(CustomErrorReportValve.class.getName());
        }
    });
}

ErrorReportValve 교체 자체는 같은 목적이지만, 적용/운영 방식은 다음처럼 다르다.

1. 설정 위치(등록 방식)

  • 외장 톰캣: server.xml의 <Host ... errorReportValveClass="..."> (운영 설정)
  • 내장 톰캣(Spring Boot): 애플리케이션 코드에서 host.setErrorReportValveClass(...) (애플리케이션 설정)

2. 배포 단위/변경 주체

  • 외장 톰캣: 톰캣 서버 설정과 라이브러리를 바꿔서 반영 (운영 측에서 통제하기 쉬움)
  • 내장 톰캣: 애플리케이션에 포함되어 배포되므로 앱 재빌드/재배포가 필요 (서비스별로 독립 통제)

3. 적용 범위

  • 외장 톰캣: 하나의 톰캣 인스턴스에 여러 WAR가 올라가면, Host 단에서 교체 시 여러 앱에 공통 적용 가능
  • 내장 톰캣: 보통 “앱 1개 = 톰캣 1개”이므로 해당 서비스에만 적용

참고 : 

tomcat8.5_valve

tomcat9_valve

tomcat9_host_errorReportValveClass

 

728x90