JavaでのSQLインジェクションと簡単な予防方法
SQLインジェクションとは何ですか?
SQLインジェクションは、トップ10のウェブアプリケーションの脆弱性の1つです。簡単に言えば、SQLインジェクションとは、ユーザーが入力したデータを介してクエリにSQLコードを注入/挿入することを意味します。Oracle、MySQL、PostgreSQL、SQL Serverなどのリレーショナルデータベースを使用するアプリケーションのいずれでも発生する可能性があります。
「SQLインジェクション」を行うために、悪意のあるユーザーはまずアプリケーション内でSQLコードをデータと共に埋め込む場所を探します。それはウェブアプリケーションのログインページや他のどこでも構いません。従って、アプリケーションがSQLコードと共にデータを受け取ると、SQLコードはアプリケーションのクエリと共に実行されます。
SQLインジェクションの影響
- A malicious user can obtain unauthorized access to your application and steal data.
- They can alter, delete data in your database and take your application down.
- A hacker can also get control of the system on which database server is running by executing database specific system commands.
SQLインジェクションはどのように機能しますか?
仮に、アプリケーションユーザーのデータを保存するtbluserというデータベーステーブルがあるとしましょう。テーブルの主キーはuserIdです。アプリケーション内には、userIdを使用して情報を取得する機能があります。userIdの値はユーザーリクエストから受け取ります。
以下のサンプルコードを見てみましょう。
String userId = {get data from end user};
String sqlQuery = "select * from tbluser where userId = " + userId;
1. 有効なユーザー入力 (Yūkōna yūzā nyūryoku)
上記のクエリが有効なデータ(つまり、ユーザーIDの値132)で実行されると、以下のようになります。
入力データ:132
実行されたクエリ:select * from tbluser where userId=132
実行されたクエリ:tbluserテーブルからuserIdが132であるものを選択する。
結果:クエリはuserIdが132のユーザーのデータを返します。この場合、SQLインジェクションは発生していません。
2. ハッカーのユーザー入力
ハッカーは、PostmanやcURLなどのツールを使用して、ユーザーのリクエストを改ざんすることができます。この方法を使えば、SQLコードをデータとして送信してUI側の検証を迂回することができます。
入力データ:2または1=1
実行されたクエリ:select * from tbluser where userId=2 or 1=1
結果:今、上記のクエリは2つの条件を持つSQL OR式を使用しています。
- userId=2: This part will match table rows having userId value as ‘2’.
- 1=1: This part will be always evaluate as true. So Query will return all the rows of the table.
SQLインジェクションの種類
SQLインジェクションの4つのタイプを見てみましょう。
1. ブール式に基づくSQLインジェクション
上記の例は、ブールベースのSQLインジェクションの一例です。真または偽を評価するブール式を使用しています。これは、データベースから追加の情報を取得するために使用されることがあります。例えば、
入力データ:2または1=1
SQLクエリ:tbl_employeeからempId=2または1=1の場合のfirst_name、last_nameを選択してください。
2. ユニオンベースのSQLインジェクション
SQLのUNION演算子は、同じ列数を持つ2つの異なるクエリからデータを結合します。この場合、UNION演算子は他のテーブルからデータを取得するために使用されます。
入力データ:2 UNION SELECT ユーザー名、パスワード FROM tbluser
クエリ:empId=2のtbl_employeeからfirst_name、last_nameを選択してください。さらに、tbluserからusername、passwordを選択してください。
ユニオンベースのSQLインジェクションを使用すれば、攻撃者はユーザーの資格情報を入手することができます。
3. 時間に基づくSQLインジェクション
タイムベースのSQLインジェクションでは、クエリに特殊な関数が注入され、指定された時間だけ実行が一時停止されます。この攻撃はデータベースサーバーの動作を遅くし、アプリケーションのパフォーマンスに影響を与えてダウンさせることがあります。例えば、MySQLでは次のような攻撃があります:
入力データ:2 + SLEEP(5)
クエリ:tbl_employeeからemp_id、first_name、last_nameを選択して、empId=2 + SLEEP(5)に一致するものを取得してください。
上記の例では、クエリの実行が5秒間一時停止します。
4. エラーに基づくSQLインジェクション
このバリエーションでは、攻撃者はエラーコードやメッセージなどの情報をデータベースから取得しようとします。攻撃者は構文的に間違ったSQLを注入するため、データベースサーバーはエラーコードとメッセージを返し、それを利用してデータベースやシステムの情報を得ることができます。
JavaのSQLインジェクションの例
シンプルなJavaウェブアプリケーションを使用して、SQLインジェクションをデモンストレーションします。私たちはLogin.htmlを持っています。これはユーザーからユーザー名とパスワードを受け取り、それらをLoginServletに送信する基本的なログインページです。
LoginServletはリクエストからユーザー名とパスワードを取得し、それをデータベースの値と照合します。認証が成功した場合、Servletはユーザーをホームページにリダイレクトします。認証が失敗した場合はエラーを返します。
ログイン.htmlのコード:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Sql Injection Demo</title>
</head>
<body>
<form name="frmLogin" method="POST" action="https://localhost:8080/Web1/LoginServlet">
<table>
<tr>
<td>Username</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>Password</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">Login</button></td>
</tr>
</table>
</form>
</body>
</html>
ログインサーブレット.javaのコード:
package com.scdev.examples;
import java.io.IOException;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (Exception e) {}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
boolean success = false;
String username = request.getParameter("username");
String password = request.getParameter("password");
// Unsafe query which uses string concatenation
String query = "select * from tbluser where username='" + username + "' and password = '" + password + "'";
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);
if (rs.next()) {
// Login Successful if match is found
success = true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {}
}
if (success) {
response.sendRedirect("home.html");
} else {
response.sendRedirect("login.html?error=1");
}
}
}
データベースのクエリ [MySQL]:
create database user;
create table tbluser(username varchar(32) primary key, password varchar(32));
insert into tbluser (username,password) values ('john','secret');
insert into tbluser (username,password) values ('mike','pass10');
1. ログインページから有効なユーザー名とパスワードが入力された場合
ユーザー名を入力してください:ジョン
ユーザー名を入力してください:秘密
(Usernameを入力してください:秘密)
クエリ:select * from tbluser where username=’john’ and password = ‘secret’ 【日本語での一つのオプションです】
結果:データベースにユーザー名とパスワードが存在するため、正常に認証されました。ユーザーはホームページにリダイレクトされます。
2. SQLインジェクションを利用してシステムへの認可されていないアクセスを得る
ユーザー名を入力してください:ダミー
パスワードを入力してください: ‘または’1’=’1
クエリ:username=‘dummy’およびpassword = ‘’または‘1’=‘1’の値を持つtbluserからすべてのカラムを選択してください。
結果:入力されたユーザー名とパスワードはデータベースに存在しませんが、認証は成功しています。なぜでしょうか?
パスワードに’ or ‘1’=’1と入力しているため、SQLインジェクションが発生しています。クエリには3つの条件があります。
-
- ユーザ名 ‘dummy’ はテーブル内に存在しないため、false として評価されます。
-
- パスワード ” はテーブル内に空のパスワードが存在しないため、false として評価されます。
- ‘1’ = ‘1’ は固定された文字列の比較であるため、true として評価されます。
今、3つの条件を組み合わせると、偽かつ偽または真の場合、最終結果は真となります。
上記のシナリオでは、ブール式を使用してSQLインジェクションを実行しました。SQLインジェクションを行う他の方法もあります。次のセクションでは、JavaアプリケーションでSQLインジェクションを防止する方法を見ていきます。
Javaコード内でのSQLインジェクションの防止
クエリを実行するために、Statementの代わりにPreparedStatementを使用するのが最も簡単な解決策です。
クエリーにユーザー名とパスワードを連結する代わりに、PreparedStatementのセッターメソッドを使用してそれらをクエリーに提供します。
今では、リクエストから受け取ったユーザー名とパスワードの値は、単なるデータとして扱われるため、SQLインジェクションは起こりません。
改変されたサーブレットのコードを見てみましょう。
String query = "select * from tbluser where username=? and password = ?";
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
stmt = conn.prepareStatement(query);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
// Login Successful if match is found
success = true;
}
rs.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {
}
}
このケースで何が起こっているか理解しましょう。
次のクエリを日本語で書き換えます。
クエリ:select * from tbluser where username = ? and password = ?
上記のクエリの中にある「?」は位置パラメータと呼ばれます。上記のクエリには2つの位置パラメータがあります。ユーザー名とパスワードをクエリに連結するのではなく、PreparedStatementで提供されるメソッドを使用してユーザーの入力を行います。
私たちは、stmt.setString(1, username) を使用して最初のパラメータを設定し、stmt.setString(2, password) を使用して2番目のパラメータを設定しました。基になるJDBC APIは、SQLインジェクションを防ぐために値のサニタイズを行ってくれます。
SQLインジェクションを防ぐためのベストプラクティス
-
- クエリを使用する前にデータを検証してください。
-
- テーブル名や列名に一般的な単語を使用しないでください。たとえば、多くのアプリケーションでは、tbluserやtblaccountを使用してユーザーデータを保存しています。Email、firstname、lastnameは一般的な列名です。
-
- ユーザー入力として受け取ったデータを直接連結してSQLクエリを作成しないでください。
-
- アプリケーションのデータレイヤーではHibernateやSpring Data JPAなどのフレームワークを使用してください。
-
- クエリには位置パラメータを使用してください。プレーンなJDBCを使用している場合は、PreparedStatementを使用してクエリを実行してください。
-
- アプリケーションのデータベースへのアクセスを権限や許可を使用して制限してください。
-
- 機密なエラーコードやメッセージをエンドユーザーに返さないでください。
-
- 安全でないSQLコードを誤って書かないように、適切なコードレビューを行ってください。
- アプリケーション内のSQLインジェクションの脆弱性を見つけて修正するために、SQLMapなどのツールを使用してください。
That’s it for Java SQL インジェクションです。何か大切なことが抜けていなければいいのですが。
下記のリンクからサンプルのJavaウェブアプリケーションプロジェクトをダウンロードできます。
SQLインジェクションのJavaプロジェクト