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

永続性トップ

Spring5とSpringBoot2の基礎に焦点を当てた新しいLearnSpringコースを発表しました。

>>コースをチェックしてください

1.はじめに

SQLインジェクションは、最もよく知られている脆弱性の1つですが、悪名高いOWASPトップ10のリストのトップにランクされ続けており、現在はより一般的なインジェクションクラスの一部となっています。

このチュートリアルでは、脆弱なアプリケーションにつながるJavaの一般的なコーディングミスと、JVMの標準ランタイムライブラリで利用可能なAPIを使用してそれら回避する方法について説明します。また、JPA、HibernateなどのORMからどのような保護を取得できるか、またどの死角についても心配する必要があるかについても説明します。

2.アプリケーションはSQLインジェクションに対してどのように脆弱になりますか?

多くのアプリケーションでは、特定の計算を実行する唯一の方法は、別のシステムまたはコンポーネントによって実行されるコードを動的に生成することであるため、インジェクション攻撃が機能します。このコードを生成する過程で、適切なサニタイズを行わずに信頼できないデータを使用する場合、ハッカーが悪用できる可能性があります。

このステートメントは少し抽象的なように聞こえるかもしれませんので、教科書の例でこれが実際にどのように行われるかを見てみましょう。

public List 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まったく検証して、クエリにの価値を。この値が信頼できるソースからのみ得られると確信している場合、悪いことは何も起こりませんが、私たちはできますか?

この関数がアカウントリソースのRESTAPI実装で使用されていると想像してみましょう。このコードを悪用するのは簡単です。クエリの固定部分と連結すると、意図した動作を変更する値を送信するだけです。

curl -X GET \ '//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 unsafeJpaFindAccountsByCustomerId(String customerId) { String jql = "from Account where customerId = '" + customerId + "'"; TypedQuery q = em.createQuery(jql, Account.class); return q.getResultList() .stream() .map(this::toAccountDTO) .collect(Collectors.toList()); } 

以前に指摘したのと同じ問題がここにも存在します。検証されていない入力を使用してJPAクエリを作成しているため、ここでも同じ種類のエクスプロイトにさらされています。

3.予防技術

SQLインジェクションとは何かがわかったので、この種の攻撃からコードを保護する方法を見てみましょう。ここでは、Javaおよび他のJVM言語で利用できるいくつかの非常に効果的な手法に焦点を当てていますが、PHP、.Net、Rubyなどの他の環境でも同様の概念を利用できます。

データベース固有の手法を含む、利用可能な手法の完全なリストを探している人のために、OWASPプロジェクトはSQLインジェクション防止に関するチートシートを維持しています。これは、この主題についてさらに学ぶのに適した場所です。

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

この手法は、ユーザーが指定した値を挿入する必要がある場合は常に、クエリで疑問符のプレースホルダー( "?")を含むプリペアドステートメントを使用することで構成されます。これは非常に効果的であり、JDBCドライバーの実装にバグがない限り、エクスプロイトの影響を受けません。

この手法を使用するようにサンプル関数を書き直してみましょう。

public List 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を取得しました。このインターフェイスは、通常のステートメントインターフェイスをいくつかのメソッドで拡張し、クエリを実行する前にユーザー指定の値をクエリに安全に挿入できるようにします。

JPAの場合、同様の機能があります。

String jql = "from Account where customerId = :customerId"; TypedQuery 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の場合と同じですが、ステートメントが少ないので便利です。

As a bonus, this approach usually results in a better performing query, since most databases can cache the query plan associated with a prepared statement.

Please note that this approach only works for placeholders used asvalues. For instance, we can't use placeholders to dynamically change the name of a table:

// This WILL NOT WORK !!! PreparedStatement p = c.prepareStatement("select count(*) from ?"); p.setString(1, tableName);

Here, JPA won't help either:

// This WILL NOT WORK EITHER !!! String jql = "select count(*) from :tableName"; TypedQuery q = em.createQuery(jql,Long.class) .setParameter("tableName", tableName); return q.getSingleResult(); 

In both cases, we'll get a runtime error.

The main reason behind this is the very nature of a prepared statement: database servers use them to cache the query plan required to pull the result set, which usually is the same for any possible value. This is not true for table names and other constructs available in the SQL language such as columns used in an order by clause.

3.2. JPA Criteria API

Since explicit JQL query building is the main source of SQL Injections, we should favor the use of the JPA's Query API, when possible.

For a quick primer on this API, please refer to the article on Hibernate Criteria queries. Also worth reading is our article about JPA Metamodel, which shows how to generate metamodel classes that will help us to get rid of string constants used for column names – and the runtime bugs that arise when they change.

Let's rewrite our JPA query method to use the Criteria API:

CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery cq = cb.createQuery(Account.class); Root root = cq.from(Account.class); cq.select(root).where(cb.equal(root.get(Account_.customerId), customerId)); TypedQuery q = em.createQuery(cq); // Execute query and return mapped results (omitted)

Here, we've used more code lines to get the same result, but the upside is that now we don't have to worry about JQL syntax.

Another important point: despite its verbosity, the Criteria API makes creating complex query services more straightforward and safer. For a complete example that shows how to do it in practice, please take a look at the approach used by JHipster-generated applications.

3.3. User Data Sanitization

Data Sanitization is a technique of applying a filter to user supplied-data so it can be safely used by other parts of our application. A filter's implementation may vary a lot, but we can generally classify them in two types: whitelists and blacklists.

Blacklists, which consist of filters that try to identify an invalid pattern, are usually of little value in the context of SQL Injection prevention – but not for the detection! More on this later.

Whitelists, on the other hand, work particularly well when we can define exactly what is a valid input.

Let's enhance our safeFindAccountsByCustomerId method so now the caller can also specify the column used to sort the result set. Since we know the set of possible columns, we can implement a whitelist using a simple set and use it to sanitize the received parameter:

private static final Set VALID_COLUMNS_FOR_ORDER_BY = Collections.unmodifiableSet(Stream .of("acc_number","branch_id","balance") .collect(Collectors.toCollection(HashSet::new))); public List 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 }

Here, we're combining the prepared statement approach and a whitelist used to sanitize the orderBy argument. The final result is a safe string with the final SQL statement. In this simple example, we're using a static set, but we could also have used database metadata functions to create it.

We can use the same approach for JPA, also taking advantage of the Criteria API and Metadata to avoid using String constants in our code:

// Map of valid JPA columns for sorting final Map
    
      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 orderByAttribute = VALID_JPA_COLUMNS_FOR_ORDER_BY.get(orderBy); if (orderByAttribute == null) { throw new IllegalArgumentException("Nice try!"); } CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery cq = cb.createQuery(Account.class); Root root = cq.from(Account.class); cq.select(root) .where(cb.equal(root.get(Account_.customerId), customerId)) .orderBy(cb.asc(root.get(orderByAttribute))); TypedQuery q = em.createQuery(cq); // Execute query and return mapped results (omitted)
    

This code has the same basic structure as in the plain JDBC. First, we use a whitelist to sanitize the column name, then we proceed to create a CriteriaQuery to fetch the records from the database.

3.4. Are We Safe Now?

Let's assume that we've used parameterized queries and/or whitelists everywhere. Can we now go to our manager and guarantee we're safe?

Well… not so fast. Without even considering Turing's halting problem, there are other aspects we must consider:

  1. Stored Procedures: These are also prone to SQL Injection issues; whenever possible please apply sanitation even to values that will be sent to the database via prepared statements
  2. Triggers: Same issue as with procedure calls, but even more insidious because sometimes we have no idea they're there…
  3. Insecure Direct Object References: Even if our application is SQL-Injection free, there's still a risk that associated with this vulnerability category – the main point here is related to different ways an attacker can trick the application, so it returns records he or she was not supposed to have access to – there's a good cheat sheet on this topic available at OWASP's GitHub repository

In short, our best option here is caution. Many organizations nowadays use a “red team” exactly for this. Let them do their job, which is exactly to find any remaining vulnerabilities.

4. Damage Control Techniques

As a good security practice, we should always implement multiple defense layers – a concept known as defense in depth. The main idea is that even if we're unable to find all possible vulnerabilities in our code – a common scenario when dealing with legacy systems – we should at least try to limit the damage an attack would inflict.

Of course, this would be a topic for a whole article or even a book but let's name a few measures:

  1. Apply the principle of least privilege: Restrict as much as possible the privileges of the account used to access the database
  2. Use database-specific methods available in order to add an additional protection layer; for example, the H2 Database has a session-level option that disables all literal values on SQL Queries
  3. Use short-lived credentials: Make the application rotate database credentials often; a good way to implement this is by using Spring Cloud Vault
  4. Log everything: If the application stores customer data, this is a must; there are many solutions available that integrate directly to the database or work as a proxy, so in case of an attack we can at least assess the damage
  5. WAFまたは同様の侵入検知ソリューションを使用します。これらは典型的なブラックリストの例です。通常、既知の攻撃シグネチャの大規模なデータベースが付属しており、検出時にプログラム可能なアクションをトリガーします。一部には、何らかのインストルメンテーションを適用することで侵入を検出できるJVM内エージェントも含まれています。このアプローチの主な利点は、完全なスタックトレースを利用できるため、最終的な脆弱性の修正がはるかに簡単になることです。

5。結論

この記事では、JavaアプリケーションのSQLインジェクションの脆弱性(ビジネスのデータに依存する組織にとって非常に深刻な脅威)と、簡単な手法を使用してそれらを防ぐ方法について説明しました。

いつものように、この記事の完全なコードはGithubで入手できます。

永続性の底

Spring5とSpringBoot2の基礎に焦点を当てた新しいLearnSpringコースを発表しました。

>>コースをチェックしてください