スタックトレースの露出

クライアント応答に含まれる内部スタックトレース

説明

例外発生時の内部スタックトレースをクライアントへそのまま返すと、クラスやパッケージの構造、ファイルパス、メソッドの呼び出し順序、フレームワークやライブラリのバージョンを推測できる情報が漏れるおそれがあります。攻撃者は境界値などの入力で意図的に例外を起こし、収集した情報をSQLインジェクションやパストラバーサルなどの攻撃に利用できます。

想定される影響

  • 内部構造の露出: クラス名、呼び出し経路、ファイルシステムのパスが、防御を回避する手掛かりになります。
  • フレームワークやバージョンの特定: 使用中のライブラリを推測され、既知のCVEを狙った攻撃につながるおそれがあります。
  • 攻撃の具体化: エラー箇所や入力の流れから、SQLインジェクション、リモートコード実行、パス操作などの攻撃を組み立てやすくなります。
  • 設定情報の漏えい: ホスト名、設定値、開発環境と本番環境の違いがメッセージに含まれる場合があります。

対処方法

  • クライアントには「リクエストを処理できません」などの一般的なメッセージを返し、スタックトレースを含めないでください。
  • 詳細は logger.error("message", ex) などでサーバーログにのみ記録し、アクセス制御と保存期間を設定してください。
  • Servlet Filter、Springの @ControllerAdvice、JAX-RSの ExceptionMapper など、共通の例外処理層で応答を統一してください。
  • 本番環境では詳細エラーを無効にしてください。例えばSpring Bootでは server.error.include-stacktrace=never を設定し、独自のエラーページを用意します。
  • ユーザー向けメッセージに、クラス名、パス、SQL、設定値が含まれていないか確認してください。

例

変更前

java
import javax.servlet.http.*;
import java.io.IOException;

public class ErrorEchoServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        try {
            String v = req.getParameter("div");
            if (v != null) {
                int n = Integer.parseInt(v);
                int r = 100 / n; // 0 입력 시 예외 발생 가능
                resp.getWriter().println("result=" + r);
            }
        } catch (Exception ex) {
            // BAD: 스택 트레이스를 사용자 응답에 직접 출력
            ex.printStackTrace(resp.getWriter());
        }
    }
}

変更後

java
import javax.servlet.http.*;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.*;

public class ErrorSafeServlet extends HttpServlet {
    private static final Logger LOG = Logger.getLogger(ErrorSafeServlet.class.getName());

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        try {
            String v = req.getParameter("div");
            if (v != null) {
                int n = Integer.parseInt(v);
                int r = 100 / n;
                resp.getWriter().println("ok");
                return;
            }
            resp.getWriter().println("ok");
        } catch (Exception ex) {
            String errorId = UUID.randomUUID().toString();
            // 서버 로그에만 상세 스택 트레이스 기록
            LOG.log(Level.SEVERE, "Unhandled exception, errorId=" + errorId, ex);
            // 사용자에게는 일반화된 오류 메시지 전송
            resp.resetBuffer();
            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "요청 처리 중 오류가 발생했습니다. (id=" + errorId + ")");
        }
    }
}

解説:

  • 変更前: ex.printStackTrace(resp.getWriter()) がスタックトレースをHTTP応答へ書き込み、内部の名前、パス、呼び出しの流れを公開します。
  • 変更後: スタックトレースはサーバーのログに記録します。クライアントには一般的なメッセージと内部情報を含まない相関IDを返し、運用担当者がログを追跡できるようにします。

参考資料