SQLインジェクション
SQLインジェクションに注目する時が来た。長い間、SQLインジェクションはOWASPのトップ10で文句なしの王者だった。SQLインジェクションは非常に古く(20年以上前)、そのリストのトップの座から少し落ちたとはいえ、いまだに信じられないほど人気があり、危険な脆弱性です。
ウェブ・セキュリティの脆弱性の一つであるSQLインジェクション(SQLi)は、攻撃者がデータベースを操作し、そこから重要な情報を引き出すことを可能にするため、今でも最もよく使われる「ハッキング」テクニックの一つである。さらに憂慮すべきことに、攻撃者は自分自身をデータベース・サーバーの管理者にして、データベースを破壊したり、トランザクションを操作したり、データを開示したり、さらに多くの問題に対して脆弱にするなど、本当に破壊的なことを行うことができる。
それがどのようにして起こるのか、簡単に見てみよう。
SQL(Structured Query Language)は、リレーショナル・データベースとの通信に使用される言語であり、開発者、データベース管理者、アプリケーションが日々生成される膨大な量のデータを管理するために使用するクエリ言語である。
アプリケーション内には、データ用とコード用の2つのコンテキストが存在する。コードコンテキストはコンピュータに何を実行すべきかを指示し、処理されるデータから分離します。SQL インジェクションは、攻撃者がデータを入力し、それが SQL インタープリタによって誤ってコードとして扱われ、アプリケーショ ンから貴重な情報を収集されることによって発生します。
SQLインジェクション攻撃の影響
SQL インジェクションは、どのようなウェブ・アプリケーションにとっても非常に有害であり、攻撃者に重要なデータへの無許可のアクセスを提供するため、多くの有名な情報漏えいの背後で好んで使われてきた手法です。攻撃者は、ユーザ名やパスワードのようなものから、クレジットカードの詳細や個人識別番号に至るまで、非常に多くの情報を見ることができます。
このデータにアクセスした攻撃者は、アカウントを乗っ取ったり、パスワードをリセットしたり、オンラインショッピングを楽しんだり、その他の(もっと悪質な)詐欺を働くことができる。
しかし、おそらくSQLiで最も憂慮すべきことは、攻撃者が発見されなければ、システムへのバックドアを長期間維持できることだ。ご想像の通り、バックドアが開いている限り、データ漏洩が繰り返されることになる。恐ろしいことだ。
これが実際にどのように見えるか、いくつかの例を見て理解を深めよう。
SQLiの例
SQLiには、様々な状況に対応できる様々な脆弱性テクニックがある。以下に挙げるのは、最も一般的なSQLiの例です:
SQLiタイプ
では、3つの異なるSQLiタイプを見てみよう。
インバンドSQLi
これはSQLインジェクションの最も一般的で、単純かつ効率的なタイプの1つである。このタイプの攻撃では、攻撃と結果の取得に同じ通信チャネルが使われます。
以下は、帯域内SQLi攻撃の2つのタイプである:
- ユニオン・ベースのSQLi- ユニオン・ベースの攻撃は、SELECT文のような2つ以上のSQLクエリを組み合わせるために、ユニオン演算子を利用します。
- エラーベースのSQLi - 攻撃者はデータベースのエラーメッセージを利用して、データベースの構造を理解します。この攻撃では、攻撃者は偽のリクエストを送信したり、サーバーにエラーメッセージを表示させるためのアクションを実行することで、データベースの情報を受け取ることができます。このため、開発者はエラーやログメッセージの送信をライブ環境では避けることが重要です。
推測SQL
推測攻撃やブラインドSQLi攻撃はより複雑で、攻略に時間がかかる。その上、攻撃者は実際に攻撃結果をすぐには得られないので、ブラインド攻撃となる。
攻撃者は、ユーザのデータベースを再構築するために、データベースサーバに HTTP リクエストでペイロードを送信します。
これらは推測的SQLi攻撃の2つのタイプである:
- ブール値ベースのブラインドSQLi- この攻撃では、ブール値(真か偽か)の結果を得るクエリをデータベースに送信し、攻撃者はHTTPレスポンスを観察してブール値の結果を予測する。
- 時間ベースのブラインドSQLi- この攻撃では、攻撃者はクエリをデータベースに送信し、レスポンスを送信する前に数秒間待たせ、攻撃者はHTTPリクエストのレスポンスタイムからクエリ結果を判断する。
帯域外SQLi
この攻撃は、データベース・サーバーの有効化された機能に依存する、よりまれなタイプのSQLi攻撃である。
。例えば、インバンド攻撃と同じ通信チャネルを使用できない場合や、HTTPレスポンスがクエリ結果を特定するのに十分明確でない場合などです。
さらに、攻撃者に必要なデータを送信するためにHTTPまたはDNSリクエストを行うデータベースサーバーの能力に大きく依存しているため、それほど一般的ではありません。
SQLiに対する防御方法
ありがたいことに、SQLインジェクションがこれほど古く、これほど一般的であることの明るい兆しは、SQLインジェクションの発生を防ぐ方法があるということだ。この種の防止テクニックを使うことは、良いコーディング習慣であるだけでなく、SQLiに対する組織のセキュリティを強化することになる。
この種の攻撃からデータベース・サーバーを守るには、入力の検証、ウェブ・アプリケーション・ファイアウォール(WAF)の使用、データベースの保護、サードパーティのセキュリティ・チームやシステムの採用、確実なSQLクエリの記述など、複数の方法がある。
PythonでSQLインジェクションを防ぐ例を見てみましょう。
Pythonの例
この例では、攻撃者はブーリアンベースのブラインドSQLインジェクションを使って、システムから重要な情報を奪おうとする。
パイソン脆弱
データベースに "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'; DROP TABLE sample_data;のように入力すると、データベースに大きな影響を与える。
パイソン修復
この攻撃を防ぐには、SQL文を以下のように変更する必要がある:
cur.execute("SELECT * FROM sample_data WHERE 名前 = %s;", (名前,))
これでシステムは、ユーザーが何らかのSQLクエリーを注入しようとしても、ユーザー入力を文字列として扱い、ユーザー入力を名前の値としてのみ扱います。
この単純な変更により、将来のクエリにおける悪意ある行為を防ぎ、ユーザー入力攻撃からシステムを保護することができる。
Javaの例
この例では、アプリケーションのユーザーデータを格納する "sample_data "というデータベーステーブルも使用します。
基本的なログイン・ページはユーザー名とパスワードを受け取り、サーブレット(LoginServlet)であるJavaファイルは、ログイン操作を許可するためにデータベースに対してそれらを検証する。
Java脆弱な例
データベース内の "sample_data "テーブルを使い、認証情報を入力としてユーザーにログイン操作を実行させる。
LoginServletファイルには、ログイン操作に対応するためのクエリがある:
//Bad Example. Do not use string concatenation.
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()) {
// 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");
}
}
以下はユーザーログインのクエリーです:
select * from sample_data where username='username' and password ='password'
SQLインジェクション
入力が有効であれば、システムは完璧に機能する。例えば、ユーザー名をAlicia、パスワードをsecretとします。
システムはこれらの認証情報を持つユーザーのデータを返す。しかし、攻撃者はSQLインジェクションのためにPostmanとcURLを使ってユーザーリクエストを操作することができます。
例えば、ハッカーはダミーのユーザー名(Alicia)とパスワード'または'1'='1'を送ることができる。
この場合、ユーザー名とパスワードは一致しませんが、条件'1'='1'は常に真になるので、ログイン操作は成功します。
ジャワ島予防
これを防ぐためには、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 - 名前付きクエリ + ネイティブクエリ
Hibernateは `Native Query` と `Named Query` の2つの方法でクエリを安全に作成することができる。どちらもパラメータの場所を指定できる。
@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 - Secure: jplq
jplq リポジトリのインターフェースに `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);
ジャバスクリプト - Secure: pg
pg` ライブラリを使用する場合、`query` メソッドは 2 番目のパラメータでパラメータを指定することができる。
const { posts }.= await db.query('SELECT * FROM Post WHERE state = $1 AND author = $2', [state, author])
Javascript - Secure:シーケライズ
sequelize`ライブラリは、第2引数でクエリのパラメータを設定することができます。これには、パラメータとしてクエリにバインドする値のリストが含まれます。