Featured image of post 【SpringBoot2】エラーレスポンスを独自カスタマイズする方法【DefaultErrorAttributes】

【SpringBoot2】エラーレスポンスを独自カスタマイズする方法【DefaultErrorAttributes】

910文字

SpringBoot2のデフォルトのエラーレスポンスは危険

脆弱性の原因

SpringBoot2でシステム開発をする際は、APIを開発する事が多いと思います。

@RestControllerをつけることで簡単に実装を進める事が出来るので、とても便利なフレームワークですよね。

しかし、何も気にせずそのまま実装を進めると、システムエラーが発生した際に、システム内部の情報が含まれたレスポンスが返却されてしまうので、脆弱性に繋がる可能性があります。

デフォルトのエラーレスポンス

実際にサーバー内部でエラーが起きると以下のようなレスポンスが返ります。

エラーレスポンス
{
    "timestamp": "2019-04-23T09:00:46.014+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "No message available",
    "trace": "java.lang.NullPointerException\n\tat tech.blogenist.service.account.api.service.account.detail.AccountDetailService.findBy(AccountDetailService.java:23)\n\tat tech.blogenist.service.account.api.controller.account.detail.AccountDetailController.detail(AccountDetailController.java:28)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.lang.reflect.Method.invoke(Method.java:498)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:634)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:200)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.lang.Thread.run(Thread.java:745)\n",
    "path": "/v1/accounts/999/"
}

パッケージ情報やエラー詳細、場合によってはパラメーターやDBのデータも出力されてしまうためとても危険です。

カスタマイズ可能

そのままだと危険ですが、もちろんエラーレスポンスをカスタマイズすることが出来ます。

今回はそのやり方をご紹介致します。

手順

DefaultErrorAttributesの継承クラスを用意する

やり方はとても簡単です。

以下のようなDefaultErrorAttributes継承クラスを用意して、getErrorAttributesメソッドをOverrideすれば良いだけです。

今回は試しにpathtimestampだけを返すようにしてみました。
@Componentを忘れずに!

CustomErrorAttributes
package tech.blogenist.service.account.api.infrastructure.error.attributes;

import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;

@Component
public class CustomErrorAttributes extends DefaultErrorAttributes
{
    public static final String TIME_STAMP     = "timestamp";
    public static final String PATH           = "path";

    private final String       PATH_ATTRIBUTE = "javax.servlet.error.request_uri";

    @Override
    public Map< String, Object > getErrorAttributes( WebRequest webRequest, boolean includeStackTrace )
    {
        Map< String, Object > customErrorAttribute = new HashMap< String, Object >();
        String path = ( String ) webRequest
                .getAttribute( PATH_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST );
        customErrorAttribute.put( TIME_STAMP, OffsetDateTime.now() );
        customErrorAttribute.put( PATH, path );

        return customErrorAttribute;

    }

}

確認

では、もう一度同じエラーを発生させてみましょう。

エラーレスポンス
{
    "path": "/v1/accounts/999/",
    "timestamp": "2019-04-23T15:08:47.866+0000"
}

しっかりとカスタムエラーレスポンスが適用されるようになり、システム内部のエラーの詳細が返却されないようになりましたね♪

参考

Spring Boot 2 プログラミング入門

終わりに

パッケージ情報やエラー詳細は脆弱性やハッキングに繋がるので必ずレスポンスに含まれないようにしましょう。

また、レスポンス項目も自由に追加することが可能なので、要件にあったエラーレスポンスを考えてみてください♪