SQLインジェクション
SQLインジェクションについて見ていきましょう。長年にわたり、OWASP Top 10の不動の王者として君臨し続けてきた脆弱性です。登場から20年以上が経過し、リストの首位からはわずかに順位を下げたものの、依然として非常に一般的かつ危険な脆弱性であることに変わりはありません。
Webセキュリティの脆弱性の中でも、SQLインジェクション(SQLi)は攻撃者が最も頻繁に利用する「ハッキング」手法の一つです。データベースを操作し、重要な情報を抽出することが可能だからです。さらに恐ろしいことに、攻撃者はデータベースサーバーの管理者権限を奪取し、データベースの破壊、トランザクションの改ざん、データの漏洩、さらには他の攻撃に対する脆弱性を生み出すといった壊滅的な被害をもたらす可能性があります。
攻撃がどのように発生するのか、簡単に見ていきましょう。
SQL(Structured Query Language)は、リレーショナルデータベースと通信するための言語です。開発者やデータベース管理者、アプリケーションが日々生成される膨大なデータを管理するために使用するクエリ言語です。
アプリケーション内には、「データ」と「コード」という2つのコンテキストが存在します。コードコンテキストはコンピュータに実行内容を指示するものであり、処理対象となるデータとは分離されています。SQLインジェクションは、攻撃者が入力したデータがSQLインタープリタによって誤ってコードとして解釈されることで発生し、これにより攻撃者はアプリケーションから貴重な情報を収集できるようになります。
SQLインジェクション攻撃の影響
SQLインジェクションはあらゆるWebアプリケーションに甚大な被害をもたらす可能性があり、多くの大規模な情報漏洩事件で利用されてきた手法です。攻撃者に重要なデータへの不正アクセスを許してしまうためです。ユーザー名やパスワードから、クレジットカード情報、個人識別番号に至るまで、膨大な情報が盗み出される恐れがあります。
これらのデータにアクセスした攻撃者は、アカウントの乗っ取りやパスワードのリセット、不正なオンラインショッピング、あるいはさらに深刻な種類の詐欺行為を行う可能性があります。
しかし、SQLiの最も恐ろしい点は、攻撃者が検知されなければ、長期間にわたってシステムへのバックドアを維持できる可能性があることです。想像に難くないように、バックドアが開かれている限り、データ漏洩が繰り返されることになります。非常に危険な脅威です。
実際にどのような形で発生するのか、いくつかの例を見て理解を深めましょう。
SQLiの例
SQLiには、状況に応じて使い分けられるさまざまな脆弱性攻撃手法が存在します。以下に、最も一般的なSQLiの例をいくつか挙げます。
SQLiの種類
それでは、SQLiの3つの主要なタイプについて見ていきましょう。
インバンドSQLi
これは最も一般的でシンプル、かつ効率的なSQLインジェクションの一種です。この攻撃では、攻撃と結果の取得に同じ通信チャネルが使用されます。
インバンドSQLi攻撃には、主に以下の2つのタイプがあります。
- UnionベースのSQLi - Unionベースの攻撃は、UNION演算子を使用してSELECT文などの複数のSQLクエリを結合し、目的の情報や結果をHTTP GETレスポンスとして取得します。
- エラーベースのSQLi - 攻撃者がデータベースのエラーメッセージを利用して、その構造を把握する手法です。この攻撃では、攻撃者は不正なリクエストを送信したり、サーバーにエラーメッセージを表示させるような操作を行ったりして、データベース情報を取得します。そのため、開発者は本番環境でエラーメッセージやログメッセージをそのまま出力しないようにし、アクセス制限をかけた場所に保存することが重要です。
推論型SQLインジェクション
推論型(ブラインド)SQLインジェクション攻撃はより複雑で、実行に時間がかかる場合があります。さらに、攻撃者は攻撃の結果を即座に得ることができないため、「ブラインド(盲目的な)」攻撃と呼ばれます。
攻撃者はHTTPリクエストを通じてデータベースサーバーにペイロードを送信し、データベースの構造を操作します。その後、アプリケーションの応答や挙動を観察することで、攻撃が成功したかどうかを判断します。
推論型SQLインジェクション攻撃には、主に以下の2つのタイプがあります。
- 真偽値ベースのブラインドSQLインジェクション - この攻撃では、データベースに対して真偽値(TrueまたはFalse)の結果を返すクエリを送信し、攻撃者はHTTPレスポンスを観察してその結果を推測します。
- 時間ベースのブラインドSQLインジェクション - この攻撃では、データベースに対して意図的に応答を数秒間遅延させるクエリを送信し、HTTPリクエストの応答時間からクエリの結果を判断します。
帯域外(Out-of-band)SQLインジェクション
これは、データベースサーバーの有効な機能に依存する、より稀なタイプのSQLインジェクション攻撃です。他の攻撃手法が使えない状況で発生します。
例えば、インバンド攻撃と同じ通信チャネルを使用できない場合や、HTTPレスポンスからクエリ結果を判断するのに十分な情報が得られない場合などが挙げられます。
また、この攻撃はデータベースサーバーがHTTPやDNSリクエストを発信して攻撃者にデータを送信する機能に大きく依存するため、一般的ではありません。
SQLインジェクションへの対策
幸いなことに、SQLインジェクションは古くからある一般的な攻撃であるため、その防止策も確立されています。こうした対策を講じることは、優れたコーディング習慣であるだけでなく、組織のセキュリティを大幅に強化することにつながります。
データベースサーバーをこうした攻撃から守るには、入力値の検証、Webアプリケーションファイアウォール(WAF)の導入、データベースのセキュアな設定、サードパーティのセキュリティチームやシステムの活用、そして堅牢なSQLクエリの記述など、複数の方法があります。
Pythonにおいて、前述のセキュリティ対策の一つを用いてSQLインジェクションを防ぐ例を見てみましょう。
Pythonの例
この例では、攻撃者が真偽値ベースのブラインドSQLインジェクションを使用して、システムから重要な情報を取得しようとするケースを想定します。
Python:脆弱なコード
データベースに「sample_data」というテーブルがあると仮定します。このテーブルには、アプリケーションユーザーのユーザー名とパスワードが保存されています。
ここで、ユーザーが以下のコマンドに従ってデータベーステーブルから値を検索できるようにします。
import mysql.connector
db = mysql.connector.connect
#悪い例。避けてください!これは学習用です。
(host="localhost", user="newuser", passwd="pass", db="sample")
cur = db.cursor()
name = raw_input('Enter Name: ')
cur.execute("SELECT * FROM sample_data WHERE Name = '%s';" % name) for row in cur.fetchall(): print(row)
db.close()
SQLインジェクション
ここで、ユーザーが検索名として「Alicia」のように入力した場合、出力に問題は生じません。
しかし、ユーザーが「 Alicia’; DROP TABLE sample_data; 」のように入力すると、データベースに重大な影響を及ぼすことになります。
Python:修正方法
攻撃を防ぐには、SQL文を以下のように変更する必要があります。
cur.execute("SELECT * FROM sample_data WHERE Name = %s;", (name,))
これにより、ユーザーがSQLクエリを注入しようとしても、システムは入力を単なる文字列として扱い、nameの値としてのみ処理するようになります。
この単純な変更を行うだけで、将来的なクエリに対する悪意のある操作を防ぎ、ユーザー入力による攻撃からシステムを保護できます。
Javaの例
この例でも、アプリケーションのユーザーデータを格納する「sample_data」というデータベーステーブルを使用します。
基本的なログインページではユーザー名とパスワードを受け取り、サーブレット(LoginServlet)であるJavaファイルがデータベースと照合してログインを許可します。
Java:脆弱な例
データベースの「sample_data」テーブルを使用し、ユーザーから認証情報を受け取ってログイン処理を行うシステムを想定します。
LoginServletファイルには、ログイン処理を行うために以下のクエリが記述されています。
//悪い例。文字列連結は使用しないでください。
String query = "select * from sample_data 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()) {
// 一致が見つかればログイン成功
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");
}
}
ユーザーログイン用のクエリは以下の通りです。
select * from sample_data where username='username' and password ='password'
SQLインジェクション
入力が正当であれば、システムは完璧に動作します。例えば、ユーザー名を「Alicia」、パスワードを「secret」とします。
システムはこれらの認証情報を持つユーザーのデータを返します。しかし、攻撃者はPostmanやcURLを使用してユーザーリクエストを操作し、SQLインジェクションを試みることができます。
例えば、ハッカーはダミーのユーザー名(Alicia)と、パスワードとして「' or '1'='1」を送信します。
この場合、ユーザー名とパスワードは一致しませんが、「'1'='1'」という条件は常に真となるため、ログイン処理が成功してしまいます。
Java:防止策
SQLインジェクションを防ぐには、LoginValidationコードを修正し、クエリ実行にStatementではなくPreparedStatementを使用する必要があります。これにより、ユーザー名とパスワードをクエリ内で連結するのではなく、セッターデータとして扱うことで、SQLインジェクションを回避できます。
以下は、修正後のLoginValidationコードです。
String query = "select * from sample_data 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()) {
success = true;
}
rs.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {
}
}
この場合、PreparedStatement、セッター、および基盤となるJDBC APIがユーザー入力を適切に処理するため、SQLインジェクションが防止されます。

例
次に、さまざまな言語での実装例を見て、実際の動作をより深く理解しましょう。
C# - 安全でない例
この例は`FromRawSql`を使用しているため安全ではありません。このメソッドはパラメータをバインドせず、エスケープ処理も行いません。そのため、このメソッドの使用は極力避けるべきです。
var blogs = context.Posts
.FromRawSql("SELECT * FROM Posts WHERE state = {0} AND author = {1}", state, author)
.ToList();
C# - 安全な実装
この例は`FromSqlInterpolated`を使用しているため安全です。補間された値をパラメータ化して処理します。
これは一般的に安全ですが、安全ではない`FromRawSql`と非常に似た記述になるリスクがあります。
var blogs = context.Posts
.FromSqlInterpolated($"SELECT * FROM Posts WHERE state = {state} AND author = {author}")
.ToList();
Java - 安全な実装: Hibernate - Named Query + Native Query
Hibernateでは、`Native Query`と`Named Query`を使用して安全にクエリを構築できます。どちらもパラメータの場所を指定することが可能です。
@NamedNativeQuery(
name = "find_post_by_state_and_author",
query =
"SELECT * " +
"FROM Post " +
"WHERE state = :state" +
" AND author = :author",
resultClass = Post.class)
java
List<Post> posts = session.createNativeQuery(
"SELECT * " +
"FROM Post " +
"WHERE state = :state" +
" AND author = :author" )
.addEntity(Post.class)
.setParameter("state", state)
.setParameter("author", author)
.list();
Java - 安全な実装: JPQL
JPQLリポジトリインターフェースで`Query`アノテーションを使用すると、さまざまな形式でクエリを定義でき、パラメータ化も行われます。
@Query("SELECT p FROM Post p WHERE u.state = ?1 and u.author = ?2")
Post findPostByStateAndAuthor(String state, int author);
@Query("SELECT p FROM Post p WHERE u.state = :state and u.author = :author")
User findPostByStateAndAuthor(@Param("state") String state, @Param("author") int author);
Javascript - 安全な実装: pg
`pg`ライブラリを使用する場合、`query`メソッドの第2引数にパラメータ値を渡すことで、クエリのパラメータ化が可能になります。
const { posts } = await db.query('SELECT * FROM Post WHERE state = $1 AND author = $2', [state, author])
Javascript - セキュアな実装: Sequelize
`sequelize`ライブラリでは、クエリの第2引数に設定を渡すことでクエリをパラメータ化できます。これには、名前またはインデックスを使用してクエリにバインドする値のリストが含まれます。
await sequelize.query(
'SELECT * FROM Post WHERE state = $state AND author = $author',
{
bind: { state: state, author: author},
type: QueryTypes.SELECT
}
);
await sequelize.query(
'SELECT * FROM Post WHERE state = $1 AND author = $2',
{
bind: [state, author],
type: QueryTypes.SELECT
}
);
実践的なセキュアコーディング
OWASP Top 10のようなソフトウェアの脆弱性を理解し軽減するためのセキュアコーディングガイドラインを確認しましょう。また、実践的なアプリシミュレーションを通じて学べるトレーニングミッションにもぜひ挑戦してください。