Webセキュリティの小部屋

Twitter のフォローはこちらから Facebook ページはこちら RSSフィードのご登録はこちらから
公開日:2013年3月6日
最終更新日:2020年8月18日

クロスサイト・リクエストフォージェリ(CSRF)対策(Java編)

入力→確認→登録→結果 という画面遷移を行う Webアプリケーションを作成し、Java におけるCSRF 対策の具体例を紹介します。画面遷移は下図のようになります。

トークンを設定しているのが登録確認画面だけですが、これは CSRF 対策では重要な処理を行う際にトークンの検証を行うためです。なお、入力画面でトークンを使用しても間違いではありません。

画面遷移図

以前はトークンにセッション ID を使用することが多かったですが、最近は暗号論的擬似乱数生成器を使用して作成した乱数をトークンを使用します。トークンについては以下の記事を参照してください。

なお、環境は、Java 11 + Tomcat 9 + MySQL 8 になります。

・input.jsp

入力画面です。

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
<h1>名前を入力してください</h1>
  <form action="/test/receive" method="post">
     名前:<input type="text" name="name"><br>
     <input type="submit" value="送信">
     <input type="reset" value="リセット">
  </form>
</body>
</html>

 

・receive

CSRFトークンをセッションに格納します。

package test.csrf;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@WebServlet("/Receive")
public class Receive extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public Receive() {
        super();
    }

    /**
     * CSRFトークンをセットする
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // セッションを取得
        HttpSession session = request.getSession(true);

        //トークンをセッションに保存
        session.setAttribute("token", Csrf.getCsrfToken()); // CSRFのトークンをセット

        // forward する
        ServletContext sc = getServletContext();
        RequestDispatcher rd = sc.getRequestDispatcher("/confirm.jsp");
        rd.forward(request, response);
    }
}

 

・Csrf

CSRFトークンを作成するクラスです。

package test.csrf;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class Csrf {
    private static int TOKEN_LENGTH = 16;//16*2=32バイト

    /**
     * 32バイトのCSRFトークンを作成
     */
    public static String getCsrfToken() {
      byte token[] = new byte[TOKEN_LENGTH];
      StringBuffer buf = new StringBuffer();
      SecureRandom random = null;

      try {
        random = SecureRandom.getInstance("SHA1PRNG");
        random.nextBytes(token);

        for (int i = 0; i < token.length; i++) {
          buf.append(String.format("%02x", token[i]));
        }

      } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
      }

      return buf.toString();
    }
}

・confirm.jsp

登録確認画面です。

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="org.apache.commons.text.StringEscapeUtils" %>
<%
  request.setCharacterEncoding("UTF-8");
%>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
<h1>登録しますか?</h1>
<form action="/test/register" method="post">
  名前:<%=StringEscapeUtils.escapeHtml4(request.getParameter("name")) %><br>
  <input type="submit" value="登録">
  <input type="button" value="戻る" onclick="history.back();">
  <input type="hidden" name="name" value="<%=StringEscapeUtils.escapeHtml4(request.getParameter("name")) %>">
  <input type="hidden" name="token" value="<%=StringEscapeUtils.escapeHtml4((String)session.getAttribute("token")) %>">
</form>
</body>
</html>

・register

CSRFトークンのチェックと登録処理を行います。

package test.csrf;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@WebServlet("/Register")
public class Register extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public Register() {
        super();
    }

    /**
     * CSRFチェックと登録処理を行う
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");

        // パラメータを取得
        String name = request.getParameter("name");
        String csrf_token = request.getParameter("token");

        // セッションのトークンを取得
        HttpSession session = request.getSession(true);
        String token = (String)session.getAttribute("token");

        //トークンチェック
        if (token == null || !(token.equals(csrf_token))) {
          //CSRF攻撃・エラー画面へ
          response.sendRedirect("/test/error.jsp");
          return;
        }

        Connection con = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;

        String url = "jdbc:mysql://192.168.33.10:3306/registersample?useServerPrepStmts=true&useUnicode=true&characterEncoding=utf8";
        String user = "mysql";
        String pass = "Mysql@1234";

        try {
            //DB に接続
            Class.forName("com.mysql.cj.jdbc.Driver");
            con = DriverManager.getConnection(url, user, pass);

            // 自動コミットOFF/トランザクションの開始
            con.setAutoCommit(false);

            //プレースホルダで SQL 作成
            String sql = "INSERT INTO users (name) values (?);";

            //SQL をプリコンパイル
            stmt = con.prepareStatement(sql);

            //パラメーターセット
            stmt.setString(1, name);

            //SQL 実行
            stmt.execute();

            // コミット
            con.commit();

        } catch (SQLException | ClassNotFoundException e) {
            if (con != null) {
                // ロールバック
                try {
                    con.rollback();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }

            e.printStackTrace();
        } finally {
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (stmt != null) {
                try {
                    stmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (con != null) {
                try {
                    con.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

        //登録結果画面へ
        response.sendRedirect("/test/success.jsp");
    }
}

 

・success.jsp

登録結果画面です。

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
  <h1>登録しました</h1>
</body>
</html>

 

・error.jsp

エラー画面です。

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
  <h1>処理が失敗しました</h1>
</body>
</html>

 

参考

Webアプリケーションセキュリティに関する記事は、以下のページにまとまっています。ぜひご確認ください。

更新履歴

・2020/08/18
CSRFのトークンをセッションIDから乱数に変更。
DBをSQL ServerからMySQLに変更。
XSSの脆弱性を解消。
その他改善。

・2013/03/06
初版公開


スポンサーリンク





カテゴリー:Webアプリケーションセキュリティ対策

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA