この記事は学習の参考のみを目的としています。
関連チュートリアルのアドレス:
https://zhuanlan.zhihu.com/p/397815893
https://www.freebuf.com/articles/web/339118.html
https://www.developer.com/design/how-to- jdbc アプリケーションを SQL インジェクションから保護/
概要
リレーショナル データベース管理システム (RDBMS) には、データベースとの通信に使用される SQL (構造化照会言語) と呼ばれる特定の言語があります。SQL で記述されたクエリは、データベースの内容と構造を操作するために使用されます。データベース構造を作成および変更する特定の SQL ステートメントは DDL (データ定義言語) ステートメントと呼ばれ、データベースの内容を操作するステートメントは DML (データ操作言語) ステートメントと呼ばれます。RDBMS パッケージに関連付けられたエンジンは SQL ステートメントを解析および解釈し、それに応じて結果を返します。これは、RDBMS と通信する一般的なプロセスです。SQL ステートメントが実行され、結果が返されるだけです。システムは、言語の文法的および意味的構造に準拠するステートメントの意図を判断しません。これは、誰がステートメントを実行したか、誰が出力を取得する権限を持っているかを確認するための認証または検証プロセスがないことも意味します。攻撃者は単純に悪意を持って SQL ステートメントをトリガーし、取得すべきではない情報を取得する可能性があります。たとえば、攻撃者は一見無害なクエリを使用して悪意のあるペイロードを含む SQL ステートメントを実行し、Web アプリケーションのデータベース サーバーを制御する可能性があります。
動作原理
攻撃者はこの脆弱性を悪用し、自らの利益を得るために悪用する可能性があります。たとえば、アプリケーションの認証および認可メカニズムをバイパスして、データベース全体からいわゆる安全なコンテンツを取得することが可能です。SQL インジェクションを使用すると、データベースのレコードを作成、更新、削除できます。したがって、SQL を使用すると、想像力だけでクエリを作成できます。
多くの場合、アプリケーションは、特定のレコードの取得、レポートの作成、ユーザーの認証、CRUD トランザクションなど、さまざまな目的でデータベースに対して SQL クエリを実行します。攻撃者は、アプリケーションの入力フォームで SQL 入力クエリを見つけるだけで済みます。フォームで準備されたクエリを使用して悪意のあるコンテンツをラップし、アプリケーションがクエリを起動すると、挿入されたペイロードも運ぶことができます。
理想的な状況の 1 つは、アプリケーションがユーザーにユーザー名またはユーザー ID の入力を要求することです。アプリはそこに脆弱な部分を開きます。SQL ステートメントは無意識のうちに実行される可能性があります。攻撃者は、SQL クエリの一部として使用され、データベースによって処理されるペイロードを挿入することにより、この脆弱性を悪用する可能性があります。たとえば、ログイン フォームの POST アクションのサーバー側の疑似コードは次のようになります。
uname = getRequestString("username");
pass = getRequestString("passwd");
stmtSQL = "SELECT * FROM users WHERE
user_name = '" + uname + "' AND passwd = '" + pass + "'";
database.execute(stmtSQL);
変数 'uname' および 'pass' を通じて SQL ステートメントに提供される入力は、ステートメントのセマンティクスを変更する方法で操作される可能性があるため、上記のコードは SQL インジェクションに対して脆弱です。
たとえば、MySQL と同様に、データベース サーバーに対して実行するようにクエリを変更できます。
stmtSQL = "SELECT * FROM users WHERE
user_name = '" + uname + "' AND passwd = '" + pass + "' OR 1=1";
これにより、認証をバイパスできる範囲で元の SQL ステートメントが変更されます。これはコードから防止する必要がある深刻な脆弱性です。
SQL インジェクション攻撃に対する防御
SQL インジェクション攻撃の可能性を減らす方法の 1 つは、フィルタされていないテキスト文字列を実行前に SQL ステートメントに追加できないようにすることです。たとえば、PreparedStatementを使用して、必要なデータベース タスクを実行できます。PreparedStatementの興味深い点は、文字列の代わりにプリコンパイルされた SQL ステートメントをデータベースに送信することです。これは、クエリとデータが別々にデータベースに送信されることを意味します。これにより、SQL インジェクション攻撃の根本原因が防止されます。SQL インジェクションでは、コードとデータを混合するという考え方があり、データは実際にはデータによって偽装されたコードの一部であるためです。PreparedStatementには、 *setString() などの複数のsetXYZ()メソッドがあります。※これらのメソッドは、SQL ステートメントに含まれる引用符などの特殊文字をフィルタリングするために使用されます。
たとえば、次の方法で SQL ステートメントを実行できます。
String sql = "SELECT * FROM employees WHERE emp_no = "+eno;
入力に従業員番号としてeno=10125を入力する代わりに、次のようにクエリを変更できます。
eno = 10125 OR 1=1
これにより、クエリによって返される結果が完全に変わります。
一例
以下のサンプル コードでは、 PreparedStatement を使用してデータベース タスクを実行する方法を示します。
package org.mano.example;
import java.sql.*;
import java.time.LocalDate;
public class App
{
static final String JDBC_DRIVER =
"com.mysql.cj.jdbc.Driver";
static final String DB_URL =
"jdbc:mysql://localhost:3306/employees";
static final String USER = "root";
static final String PASS = "secret";
public static void main( String[] args )
{
String selectQuery = "SELECT * FROM employees
WHERE emp_no = ?";
String insertQuery = "INSERT INTO employees
VALUES (?,?,?,?,?,?)";
String deleteQuery = "DELETE FROM employees
WHERE emp_no = ?";
Connection connection = null;
try {
Class.forName(JDBC_DRIVER);
connection = DriverManager.getConnection
(DB_URL, USER, PASS);
}catch(Exception ex) {
ex.printStackTrace();
}
try(PreparedStatement pstmt =
connection.prepareStatement(insertQuery);){
pstmt.setInt(1,99);
pstmt.setDate(2, Date.valueOf
(LocalDate.of(1975,12,11)));
pstmt.setString(3,"ABC");
pstmt.setString(4,"XYZ");
pstmt.setString(5,"M");
pstmt.setDate(6,Date.valueOf(LocalDate.of(2011,1,1)));
pstmt.executeUpdate();
System.out.println("Record inserted successfully.");
}catch(SQLException ex){
ex.printStackTrace();
}
try(PreparedStatement pstmt =
connection.prepareStatement(selectQuery);){
pstmt.setInt(1,99);
ResultSet rs = pstmt.executeQuery();
while(rs.next()){
System.out.println(rs.getString(3)+
" "+rs.getString(4));
}
}catch(Exception ex){
ex.printStackTrace();
}
try(PreparedStatement pstmt =
connection.prepareStatement(deleteQuery);){
pstmt.setInt(1,99);
pstmt.executeUpdate();
System.out.println("Record deleted
successfully.");
}catch(SQLException ex){
ex.printStackTrace();
}
try{
connection.close();
}catch(Exception ex){
ex.printStackTrace();
}
}
}
PreparedStatement を覗いてみる
これらのジョブは、JDBC ステートメント インターフェイスを使用して実行することもできますが、問題は、特にユーザー入力値と SQLクエリに接続されたデータベースにクエリを実行する動的 SQLステートメントを実行する場合に、非常に安全でない可能性があることです。ご覧のとおり、これは危険な状況になる可能性があります。ほとんどの場合、Statement は無害ですが、この 2 つのうちPreparedStatement を選択する方が良いようです。ステートメントをデータベースに送信するときに異なるアプローチが取られるため、悪意のある文字列が連結されるのを防ぎます。PreparedStatement は、連結の代わりに変数置換を使用します。SQL クエリに疑問符 (?) を入れることは、クエリの実行時に置換変数が代わりに値を提供することを意味します。置換変数の位置は、setXYZ()メソッドで割り当てられたパラメータ インデックスの位置に従って置き換えられます。
この技術により、SQL インジェクション攻撃から保護されます。
さらに、PreparedStatement はAutoCloseableを実装します。これにより、 「トライアル リソース」ブロックのコンテキストで記述することが可能になり、範囲外になると自動的に閉じられます。
結論は
SQL インジェクション攻撃は、責任を持ってコードを記述することによってのみ防ぐことができます。実際、どのようなソフトウェア ソリューションでも、セキュリティはほとんどが間違ったコーディング方法によって破られます。ここでは、何を避けるべきか、またPreparedStatement が安全なコードの作成にどのように役立つかについて説明します。SQL インジェクションの完全な概念については、インターネットに溢れている適切な資料を参照してください。また、PreparedStatementについては、Java API ドキュメントで詳細な説明を確認してください。