初心者向け:Spring BootとMyBatisで実践!SQLインジェクションのリスクと防止策

初心者向け:Spring BootとMyBatisで実践!SQLインジェクションのリスクと防止策
  • URLをコピーしました!

Webアプリケーションを開発する上で、避けて通れないのが「セキュリティ対策」。中でも「SQLインジェクション」は、データベースに対する深刻な脅威として知られています。

この記事では、SQLインジェクションの仕組みをわかりやすく解説し、実際にどのように攻撃が行われるのかをデモします。さらに、Spring BootとMyBatisを使った安全な実装例をコード付きで詳しく解説!初心者から中級者まで、Web開発者なら必見の内容です。

目次

SQLインジェクションとは?攻撃の危険性を動画で体感しよう!

SQLインジェクションは、アプリケーションの脆弱性を利用して、データベースに不正な操作を仕掛ける攻撃の一つです。
「ちょっと難しそう…」と感じた方も、以下の短い動画を見れば、どんな攻撃かをざっくり理解できるはず!

📺 YouTubeでデモを見る:こちらのリンクから視聴できます。

動画では、SQLインジェクションがどのように働くのかをシンプルな例を使って説明しています。
ぜひ見て、次のセクションで詳しい内容に進んでみてください!

SQLインジェクションとは?

SQLインジェクションは、アプリケーションがデータベースとやり取りを行う際に、攻撃者が悪意のあるSQLコードを注入することで、不正な操作を行わせる攻撃手法です。

この攻撃が成功すると、次のようなリスクがあります
  • データの不正取得: ユーザー情報や機密データが漏洩する可能性があります。
  • 認証のバイパス: 攻撃者が不正にログインできる場合があります。
  • データの改ざん・削除: データベースが操作され、信頼性が損なわれます。

具体例で見るSQLインジェクションの仕組み

ログイン機能で仕組みを説明します。例えば、下記のパラメータの場合、SQLはこのように変換されます。

  • ユーザー名: admin
  • パスワード: password
SELECT COUNT(*) > 0 FROM users WHERE username = 'admin' AND password = 'password';

これは通常の動作ですが、攻撃者が以下のような入力をした場合はどうなるでしょうか?

  • ユーザー名: ' OR '1'='1' --
  • パスワード: 任意の値

こちらは、下記のように書き換えられます。

SELECT COUNT(*) > 0 FROM users WHERE username = '' OR '1'='1' -- AND password = '任意の値';

結果: -- AND password = '任意の値'は、--によってコメントアウトされます。OR '1'='1が常に真であるため、すべてのユーザーが認証されたと見なされ、不正ログインが可能になってしまいます。

SQLインジェクションの対策方法

SQLインジェクションは、アプリケーションのセキュリティにおいて深刻な脅威です。しかし、適切な対策を講じることで、このリスクを効果的に軽減することができます。

今回は、Spring BootとMyBatisを使用して、実際にどのように対策を講じるのかを具体例とともに解説します。これらのツールを活用することで、セキュリティの高いアプリケーションを構築する方法を学びましょう。

1. プレースホルダー付きのパラメータ化クエリを使用する

概要: ユーザー入力をSQL文に直接埋め込まず、プレースホルダー(#{})を利用してパラメータを安全にバインドします。
理由: プレースホルダーにより、入力値が自動的にエスケープされ、SQLインジェクションを防止します。

下記にプレースホルダーを利用しているコードと利用していないコードを載せてます。
違いは、「’${username}’→#{username}」、「’${password}’→#{password}」の部分になります。

Mybatisの場合は、#{}にすることでパラメータを安全にバインドできます。
パラメータをそのままSQLで利用する場合は、必ず#{}を使うようにしましょう。

脆弱

@Select("SELECT COUNT(*) > 0 FROM users WHERE username = '${username}' AND password = '${password}'")
boolean vulnerableAuthenticate(@Param("username") String username, @Param("password") String password);

安全

@Select("SELECT COUNT(*) > 0 FROM users WHERE username = #{username} AND password = #{password}")
boolean secureAuthenticate(@Param("username") String username, @Param("password") String password);

2. ユーザー入力の検証とサニタイズ

概要: ユーザーが入力できる文字種や形式を制限し、不正な入力を防ぎます。
理由: 危険な文字列やSQLキーワードを除去することで、攻撃の成功率を大幅に下げられます。

パラメータの文字種や形式を制限したり、特定の文字(例: ', --, ;など)を削除またはエスケープすることで、SQLインジェクションを回避することも可能です。

if (!username.matches("^[a-zA-Z0-9]+$")) {
    throw new IllegalArgumentException("不正な入力です");
}

基本は、「1. プレースホルダー付きのパラメータ化クエリを使用する」を検討した方が良いですが、どうしても不可能な場合は、こちらの対処法で攻撃の成功率を大幅に下げられます。

3. 動的クエリの構築を避ける

概要: SQL文を文字列操作で動的に生成するのではなく、パラメータ化されたクエリを利用します。
理由: 文字列操作でSQLを構築すると、SQLインジェクションが発生しやすくなります。

こちらは、対策方法とは少し違いますが、やはり人間はミスをするものだと仮定した上で、
動的SQLを組み立てる際に文字列操作(+演算子やString.format)を使用すると、SQLインジェクションのリスクが発生してしまうことを理解した方がいいと考えています。

Mybatisの場合、デフォルトで文字列操作を使用してクエリを直接構築する方法を避ける設計になっていますので、問題はありませんが、動的SQLを組み立てる際に文字列操作を使っている場合は、注意してください。

過去にあった事例でいくと、WHERE文を文字列操作で構築して、${}で設定するソースは見かけたことがあります。
この場合、もちろんSQLインジェクションの可能性は高まります。

public String buildWhereClause(String username, String email) {
    String whereClause = "WHERE 1=1";
    if (username != null) {
        whereClause += " AND username = '" + username + "'";
    }
    if (email != null) {
        whereClause += " AND email = '" + email + "'";
    }
    return whereClause;
}

@Select("SELECT * FROM users ${whereClause}")
List<User> findUsers(@Param("whereClause") String whereClause);

Mybatisの場合、<if>で動的クエリを生成できますので、こちらを利用するようにしましょう。

<select id="findUsers" resultType="User">
    SELECT * FROM users
    WHERE 1=1
    <if test="username != null">
        AND username = #{username}
    </if>
    <if test="email != null">
        AND email = #{email}
    </if>
</select>

4. その他

他にもSQLインジェクションを軽減させる方法はいくつかありますが、完全ではなくあくまで軽減であることを理解しておきましょう。

  • 最小限のデータベース権限を設定
  • ストアドプロシージャの使用
  • Web Application Firewall(WAF)の導入
  • etc

Spring Boot + MyBatisを使用した実装例

Youtubeでも紹介していますが、実際にSQLインジェクションをデモできる実装例を載せておきます。
ソースコード全体は、GitHubに載せておりますので、必要な方は確認してみてください。

💾 GitHubリポジトリ:こちらのリンクでソースコードをチェック!

Controller

package com.youtube.security.app.security_demo.controller;

import com.youtube.security.app.security_demo.repository.SqlInjectionMapper;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * このコントローラーは、SQLインジェクションの脆弱性がどのように発生するかを示し、
 * MyBatisを使用した安全な実装例を提供します。
 *
 * SQLインジェクションは、攻撃者が悪意のあるSQLコードをクエリに注入することで、
 * 任意のSQL文をデータベース上で実行可能にする攻撃の一種です。
 *
 * 安全な実装例:
 * - プレースホルダー付きのパラメータ化されたクエリを使用する
 *  -> クエリに直接ユーザー入力を埋め込まず、プレースホルダー(#{})を使用することでSQLインジェクションを防止します。
 * - ユーザー入力の検証とサニタイズを行う
 *  -> ユーザーが入力できる文字種を制限したり、危険な文字や記号の除去・エスケープを行うことで安全性を高めます。
 * - 文字列操作での動的SQL生成を避ける
 *  -> 動的SQLを組み立てる際に文字列操作(+演算子やString.format)を使用すると、SQLインジェクションのリスクが高まります。
 *  -> クエリを文字列操作で動的に生成せず、安全な方法で組み立てることが重要です。
 *  -> Mybatisの場合、デフォルトで文字列操作を使用してクエリを直接構築する方法を避ける設計になっています。
 *
 * ※このクラスは教育目的でのみ使用してください。
 */
@RestController
public class SqlInjectionController {

    private final SqlInjectionMapper sqlInjectionMapper;

    public SqlInjectionController(SqlInjectionMapper sqlInjectionMapper) {
        this.sqlInjectionMapper = sqlInjectionMapper;
    }

    /**
     * MyBatisを使用した脆弱なクエリの例です。
     *
     * @param username ユーザー名
     * @param password パスワード
     * @return ログイン結果 (成功または失敗)
     */
    @PostMapping("/login/vulnerable")
    public String vulnerableLogin(@RequestParam String username, @RequestParam String password) {
        // MyBatisの脆弱なクエリを呼び出し
        boolean isAuthenticated = sqlInjectionMapper.vulnerableAuthenticate(username, password);
        return isAuthenticated ? "ログイン成功 (脆弱な実装)" : "ログイン失敗 (脆弱な実装)";
    }

    /**
     * MyBatisを使用してパラメータ化されたクエリを実行します。
     *
     * @param username ユーザー名
     * @param password パスワード
     * @return ログイン結果 (成功または失敗)
     */
    @PostMapping("/login/secure")
    public String secureLogin(@RequestParam String username, @RequestParam String password) {
        // MyBatisの安全なクエリを呼び出し
        boolean isAuthenticated = sqlInjectionMapper.secureAuthenticate(username, password);
        return isAuthenticated ? "ログイン成功 (安全な実装)" : "ログイン失敗 (安全な実装)";
    }
}

Mapper

package com.youtube.security.app.security_demo.repository;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface SqlInjectionMapper {

    /**
     * 脆弱なSQLクエリを実行するメソッド。
     * @param username ユーザー名
     * @param password パスワード
     * @return 認証成功ならtrue、失敗ならfalse
     */
    @Select("SELECT COUNT(*) > 0 FROM users WHERE username = '${username}' AND password = '${password}'")
    boolean vulnerableAuthenticate(@Param("username") String username, @Param("password") String password);

    /**
     * 安全なSQLクエリを実行するメソッド。
     * @param username ユーザー名
     * @param password パスワード
     * @return 認証成功ならtrue、失敗ならfalse
     */
    @Select("SELECT COUNT(*) > 0 FROM users WHERE username = #{username} AND password = #{password}")
    boolean secureAuthenticate(@Param("username") String username, @Param("password") String password);
    
}

SQLインジェクションの実践

SQLインジェクションを実践するためのツールを用意しております。
GitHubにありますので、是非活用してみてください。(使わなくても、同じリクエストを送るだけで問題はありません。)

💾 GitHubリポジトリ:こちらのリンクでソースコードをチェック!

以下のYouTube動画では、SQLインジェクション攻撃のデモを行っています。具体的な攻撃例を実際に体感できる内容となっていますので、ぜひご覧ください。

📺 YouTubeでデモを見る:こちらのリンクから視聴できます。

動画では、SQLインジェクション攻撃がどのようにアプリケーションの脆弱性を悪用するのかを実演しています。簡単なデモですが、攻撃の概要を理解する助けになるはずです。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

情報セキュリティを勉強するために始めたブログです。
新人のため、広い心を持って見ていただけると嬉しく思います。
楽しくプログラミングを勉強するために、「Teech Lab.」もありますので、ソフトウェア開発にも興味があればぜひ覗いて見てください!

目次