SQLインジェクションとそれを防ぐ方法

  • Persistence

  • link:/category/security-2/ [セキュリティ]

1. 前書き

最も有名な脆弱性の1つであるにもかかわらず、SQLインジェクションは悪名​​高いhttps://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project[OWASP Top 10's list]†"now partのトップスポットにランクされ続けています。より一般的な_Injection_クラスの。
このチュートリアルでは、JVMの標準ランタイムライブラリで利用可能なAPIを使用して、脆弱なアプリケーションにつながるJavaの一般的なコーディングの誤りとそれらを回避する方法を調べます。 また、JPA、HibernateなどのORMからどのような保護を取得できるのか、さらに心配する必要がある盲点についても説明します。

2. アプリケーションがSQLインジェクションに対して脆弱になる方法

*多くのアプリケーションでは、特定の計算を実行する唯一の方法は、別のシステムまたはコンポーネントによって実行されるコードを動的に生成することであるため、インジェクション攻撃が機能します*。 このコードを生成する過程で、適切なサニタイズを行わずに信頼できないデータを使用する場合、ハッカーが悪用する可能性を残します。
この文は少し抽象的に聞こえるかもしれないので、教科書の例を使って実際にこれがどのように起こるか見てみましょう:
public List<AccountDTO>
  unsafeFindAccountsByCustomerId(String customerId)
  throws SQLException {
    // UNSAFE !!! DON'T DO THIS !!!
    String sql = "select "
      + "customer_id,acc_number,branch_id,balance "
      + "from Accounts where customer_id = '"
      + customerId
      + "'";
    Connection c = dataSource.getConnection();
    ResultSet rs = c.createStatement().executeQuery(sql);
    // ...
}
このコードの問題は明らかです:*検証をまったく行わずに、_customerId_ ’の値をクエリに入れました*。 この値が信頼できるソースからのみ提供されると確信していれば、何も悪いことは起こりませんが、できますか?
この関数は、__account __resourceのREST API実装で使用されると想像してください。 このコードを利用するのは簡単です。必要なのは、クエリの固定部分と連結されたときに意図した動作を変更する値を送信することだけです。
curl -X GET \
  'http://localhost:8080/accounts?customerId=abc%27%20or%20%271%27=%271' \
_customerId_パラメーターの値が関数に到達するまでチェックされていない場合、以下のようになります。
abc' or '1' = '1
この値を固定部分と結合すると、実行される最終的なSQLステートメントが取得されます。
select customer_id, acc_number,branch_id, balance
  from Accounts where customerId = 'abc' or '1' = '1'
おそらく私たちが望んでいたものではありません…
賢い開発者(私たち全員ではありませんか?)は今考えています。 文字列連結を使用してこのようなクエリを作成することはありません」。
それほど高速ではありません。この標準的な例は本当にばかげていますが、*まだ必要な場合があります*:
  • 動的検索条件を使用した複雑なクエリ:UNION句の追加
    ユーザー指定の基準に応じて

  • 動的なグループ化または順序付け:GUIのバックエンドとして使用されるREST API
    データ表

2.1. JPAを使用しています。 私は安全ですよね?

*これはよくある誤解です*。 JPAや他のORMは、手作業でコーディングされたSQLステートメントを作成することから解放しますが、脆弱なコードの記述を妨げることはありません*。
前の例のJPAバージョンがどのように見えるか見てみましょう:
public List<AccountDTO> unsafeJpaFindAccountsByCustomerId(String customerId) {
    String jql = "from Account where customerId = '" + customerId + "'";
    TypedQuery<Account> q = em.createQuery(jql, Account.class);
    return q.getResultList()
      .stream()
      .map(this::toAccountDTO)
      .collect(Collectors.toList());
}
前に指摘したのと同じ問題もここにあります:*未検証の入力を使用してJPAクエリを作成している*ため、ここで同じ種類のエクスプロイトにさらされています。

3. 予防テクニック

SQLインジェクションとは何かがわかったので、この種の攻撃からコードを保護する方法を見てみましょう。 ここでは、Javaおよび他のJVM言語で利用可能な非常に効果的な手法に焦点を当てていますが、PHP、.Net、Rubyなどの他の環境でも同様の概念を利用できます。
データベース固有のものを含む利用可能な技術の完全なリストを探している人のために、https://www.owasp.org [OWASP Project]は、https://www.owasp.org/index.php/SQL_Injection_Prevention_Cheat_Sheet [SQL注入防止に関するチートシート]。このトピックについて詳しく知るのに適した場所です。

3.1。 パラメータ化されたクエリ

この手法は、ユーザーが指定した値を挿入する必要があるときはいつでも、クエリで疑問符プレースホルダー(「?」)を備えた準備済みステートメントを使用することから成ります。 これは非常に効果的であり、JDBCドライバーの実装にバグがない限り、悪用されません。
この手法を使用するようにサンプル関数を書き直しましょう。
public List<AccountDTO> safeFindAccountsByCustomerId(String customerId)
  throws Exception {

    String sql = "select "
      + "customer_id, acc_number, branch_id, balance from Accounts"
      + "where customer_id = ?";

    Connection c = dataSource.getConnection();
    PreparedStatement p = c.prepareStatement(sql);
    p.setString(1, customerId);
    ResultSet rs = p.executeQuery(sql));
    // omitted - process rows and return an account list
}
ここでは、_Connection_インスタンスで使用可能な_prepareStatement()_メソッドを使用して、_PreparedStatement_を取得しました。 このインターフェイスは、通常の__Statement __interfaceをいくつかのメソッドで拡張します。これにより、クエリを実行する前に、ユーザーが指定した値をクエリに安全に挿入できます。
JPAには、同様の機能があります。
String jql = "from Account where customerId = :customerId";
TypedQuery<Account> q = em.createQuery(jql, Account.class)
  .setParameter("customerId", customerId);
// Execute query and return mapped results (omitted)
このコードをSpring Bootで実行する場合、プロパティ_logging.level.sql_をDEBUGに設定し、この操作を実行するために実際に作成されるクエリを確認できます。
// Note: Output formatted to fit screen
[DEBUG][SQL] select
  account0_.id as id1_0_,
  account0_.acc_number as acc_numb2_0_,
  account0_.balance as balance3_0_,
  account0_.branch_id as branch_i4_0_,
  account0_.customer_id as customer5_0_
from accounts account0_
where account0_.customer_id=?
予想どおり、ORMレイヤーは、_customerId_パラメーターのプレースホルダーを使用して準備済みステートメントを作成します。 これは、単純なJDBCの場合で行ったのと同じですが、少数のステートメントが少ないので便利です。
おまけとして、ほとんどのデータベースは準備済みステートメントに関連付けられたクエリプランをキャッシュできるため、このアプローチでは通常、クエリのパフォーマンスが向上します。
*このアプローチは、*値*として使用されるプレースホルダーに対してのみ機能することに注意してください。 たとえば、プレースホルダーを使用してテーブルの名前を動的に変更することはできません。
// This WILL NOT WORK !!!
PreparedStatement p = c.prepareStatement("select count(*) from ?");
p.setString(1, tableName);
ここでは、JPAも助けにはなりません。
// This WILL NOT WORK EITHER !!!
String jql = "select count(*) from :tableName";
TypedQuery q = em.createQuery(jql,Long.class)
  .setParameter("tableName", tableName);
return q.getSingleResult();
どちらの場合も、実行時エラーが発生します。
この背後にある主な理由は、プリペアドステートメントの性質そのものです。データベースサーバーは、それらを使用して、結果セットをプルするために必要なクエリプランをキャッシュします。 これは、テーブル名や、__ order by_句で使用される列など、SQL言語で使用可能なその他の構成要素には当てはまりません。

3.2. JPA基準API

明示的なJQLクエリの構築はSQLインジェクションの主なソースであるため、可能な場合はJPAのクエリAPIの使用を優先する必要があります。
このAPIの簡単な入門書については、https://www.baeldung.com/hibernate-criteria-queriesをご覧ください(Hibernate Criteriaクエリに関する記事を参照)。 また、https://www.baeldung.com/hibernate-criteria-queries-metamodel [JPAメタモデルに関する記事]も読む価値があります。これは、列名に使用される文字列定数を取り除くのに役立つメタモデルクラスを生成する方法を示しています–および変更時に発生するランタイムバグ。
Criteria APIを使用するようにJPAクエリメソッドを書き直しましょう。
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Account> cq = cb.createQuery(Account.class);
Root<Account> root = cq.from(Account.class);
cq.select(root).where(cb.equal(root.get(Account_.customerId), customerId));

TypedQuery<Account> q = em.createQuery(cq);
// Execute query and return mapped results (omitted)
ここでは、同じ結果を得るためにさらに多くのコード行を使用しましたが、利点は* JQL構文を心配する必要がなくなったことです*。
もう1つの重要な点:冗長性にもかかわらず、Criteria APIは複雑なクエリサービスの作成をより簡単かつ安全にします。*実際の実行方法を示す完全な例については、https:// www .jhipster.tech [JHipster]で生成されたアプリケーション。

3.3. ユーザーデータのサニタイズ

*データのサニタイズは、ユーザーが提供したデータにフィルターを適用して、アプリケーションの他の部分で安全に使用できるようにする手法です*。 フィルターの実装は大きく異なる場合がありますが、通常はホワイトリストとブラックリストの2つのタイプに分類できます。
無効なパターンを識別しようとするフィルターで構成される_Blacklists_は、通常、SQLインジェクション防止のコンテキストではほとんど価値がありませんが、検出には役立ちません。 これについては後で詳しく説明します。
一方、_Whitelists_は、*有効な入力を正確に定義できる*場合に特にうまく機能します。
__safeFindAccountsByCustomerId __methodを拡張して、呼び出し元が結果セットの並べ替えに使用する列を指定できるようにします。 可能な列のセットがわかっているため、単純なセットを使用してホワイトリストを実装し、それを使用して受信したパラメーターをサニタイズできます。
private static final Set<String> VALID_COLUMNS_FOR_ORDER_BY
  = Collections.unmodifiableSet(Stream
      .of("acc_number","branch_id","balance")
      .collect(Collectors.toCollection(HashSet::new)));

public List<AccountDTO> safeFindAccountsByCustomerId(
  String customerId,
  String orderBy) throws Exception {
    String sql = "select "
      + "customer_id,acc_number,branch_id,balance from Accounts"
      + "where customer_id = ? ";
    if (VALID_COLUMNS_FOR_ORDER_BY.contains(orderBy)) {
        sql = sql + " order by " + orderBy;
    } else {
        throw new IllegalArgumentException("Nice try!");
    }
    Connection c = dataSource.getConnection();
    PreparedStatement p = c.prepareStatement(sql);
    p.setString(1,customerId);
    // ... result set processing omitted
}
ここでは、* prepared statementアプローチと、_orderBy_引数のサニタイズに使用するホワイトリストを組み合わせています*。 最終結果は、最終SQLステートメントを含む安全な文字列です。 この単純な例では、静的セットを使用していますが、データベースメタデータ関数を使用して作成することもできます。
JPAにも同じアプローチを使用できます。また、Criteria APIとメタデータを利用して、コードで_String_定数を使用しないようにすることができます。
// Map of valid JPA columns for sorting
final Map<String,SingularAttribute<Account,?>> VALID_JPA_COLUMNS_FOR_ORDER_BY = Stream.of(
  new AbstractMap.SimpleEntry<>(Account_.ACC_NUMBER, Account_.accNumber),
  new AbstractMap.SimpleEntry<>(Account_.BRANCH_ID, Account_.branchId),
  new AbstractMap.SimpleEntry<>(Account_.BALANCE, Account_.balance))
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

SingularAttribute<Account,?> orderByAttribute = VALID_JPA_COLUMNS_FOR_ORDER_BY.get(orderBy);
if (orderByAttribute == null) {
    throw new IllegalArgumentException("Nice try!");
}

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Account> cq = cb.createQuery(Account.class);
Root<Account> root = cq.from(Account.class);
cq.select(root)
  .where(cb.equal(root.get(Account_.customerId), customerId))
  .orderBy(cb.asc(root.get(orderByAttribute)));

TypedQuery<Account> q = em.createQuery(cq);
// Execute query and return mapped results (omitted)
このコードの基本構造は、プレーンJDBCと同じです。 まず、ホワイトリストを使用して列名をサニタイズし、次に_CriteriaQuery_を作成してデータベースからレコードを取得します。

3.4. 今は安全ですか?

どこでもパラメータ化されたクエリやホワイトリストを使用したと仮定しましょう。 私たちは今、マネージャーに行き、私たちが安全であることを保証できますか?
まあ…そんなに速くない。 https://en.wikipedia.org/wiki/Halting_problem [チューリングの停止問題]を考慮しなくても、考慮しなければならない他の側面があります。
  1. ストアドプロシージャこれらは、SQLインジェクションの問題が発生しやすい;
    可能な限り、準備されたステートメントを介してデータベースに送信される値にも衛生を適用してください

  2. _Triggers:_プロシージャ呼び出しと同じ問題ですが、さらに多くの問題
    陰湿なのは、彼らがそこにいるとは知らないこともあるからです。

  3. 安全でない直接オブジェクト参照:アプリケーションが
    SQL-Injectionは無料です。この脆弱性カテゴリに関連するリスクはまだあります。ここでの主なポイントは、攻撃者がアプリケーションをだますさまざまな方法に関連しているため、アクセスできないはずのレコードを返します。このトピックに関するチートシート

    要するに、ここでの最善の選択肢は注意です。 現在、多くの組織がまさにこのために「レッドチーム」を使用しています。 彼らに任せましょう。これは、残りの脆弱性を見つけることです。

4. 損傷制御技術

*優れたセキュリティ対策として、常に複数の防御層を実装する必要があります* – –_defense in depth_として知られる概念。 主なアイデアは、コードに潜在的な脆弱性をすべて見つけることができなくても(レガシシステムを扱う場合の一般的なシナリオ)、少なくとも攻撃による被害を制限することです。
もちろん、これは記事全体または本のトピックにもなりますが、いくつかの対策を挙げましょう。
  1. 最小特権の原則を適用します。できるだけ制限する
    データベースへのアクセスに使用されるアカウントの権限

  2. 追加するために利用可能なデータベース固有のメソッドを使用します
    保護層;たとえば、H2データベースには、SQLクエリのすべてのリテラル値を無効にするセッションレベルオプションがあります

  3. 短期間の資格情報を使用する:アプリケーションでデータベースを回転させる
    資格情報;
    これを実装する良い方法は、https://www.baeldung.com/spring-cloud-vault [Spring Cloud Vault]を使用することです

  4. すべてを記録:アプリケーションが顧客データを保存する場合、これは
    する必要があります;
    データベースに直接統合するかプロキシとして機能する多くのソリューションが利用可能であるため、攻撃の場合、少なくとも被害を評価できます

  5. WAFsまたは
    同様の侵入検知ソリューション:これらは典型的な_blacklist_の例です。通常、既知の攻撃シグネチャのかなり大きなデータベースが付属しており、検知時にプログラム可能なアクションをトリガーします。 一部には、何らかのインストルメンテーションを適用することで侵入を検出できるJVM内エージェントも含まれます。このアプローチの主な利点は、完全なスタックトレースを使用できるため、最終的な脆弱性の修正がはるかに簡単になることです。

5. 結論

この記事では、JavaアプリケーションのSQLインジェクションの脆弱性(ビジネスのデータに依存している組織にとって非常に深刻な脅威)と、簡単な手法を使用してそれらを防ぐ方法について説明しました。
いつものように、この記事の完全なコードはhttps://github.com/eugenp/tutorials/tree/master/software-security/sql-injection-samples[Githubで入手可能]です。