IDS00-J. SQL インジェクションを防ぐ
SQLインジェクションの脆弱性は、信頼できない入力源からの入力を使って SQL クエリを組み立てている場合に発生する。クエリを改変されることにより、データベースの情報漏えいやデータ改変につながる恐れがある。SQLインジェクションを防ぐには、パラメタ化クエリやストアドプロシージャによってデータの無害化および信頼できない入力値の検証を行うことが最も有効である。
次の場合について考えてみよう。システムはユーザを認証するために次のようなクエリを SQL データベースに対して発行する。クエリの結果が空でなければ認証成功、空であれば認証失敗とする。
SELECT * FROM db_user WHERE username='<USERNAME>' AND 
                            password='<PASSWORD>'
一方、攻撃者は <USERNAME> や <PASSWORD> の部分を任意の文字列に置き換えることができるとする。この場合、<USERNAME> の部分に次のような文字列を与えることで、ユーザ認証を回避することが可能である。
validuser' OR '1'='1
認証を行うコードは次のようなクエリを構築する:
SELECT * FROM db_user WHERE username='validuser' OR '1'='1' AND password='<PASSWORD>'
validuser が実在する正規のユーザ名であれば、この SELECT 文は validuser のレコードをテーブルから選択する。username='validuser' の評価は真となるので、パスワードはチェックされない。つまり OR 以降の部分は評価されないということである。OR 以降の部分が SQL 言語の式として文法的に正しければ、攻撃者は validuser のレコードへのアクセスを許可される。
同様に、攻撃者は次のような <PASSWORD> を入力することもできる。
' OR '1'='1
この入力からは次のようなクエリがつくられる:
SELECT * FROM db_user WHERE username='<USERNAME>' AND password='' OR '1'='1'
'1'='1' は常に真なので、このクエリからはテーブル中のすべての行が出力される。ユーザ名とパスワードの検査は行われず、攻撃者は正しいユーザIDやパスワードを知らなくてもログインできてしまう。
違反コード
以下の違反コード例は、ユーザ認証を行うJDBCのコードを示している。パスワードはchar型配列として渡され、データベースへの接続が作成され、パスワードがハッシュ化されている。
残念ながらこのコードは SQL インジェクション攻撃を許している。入力 username をサニタイズせずにそのまま SQL クエリに使っているため、攻撃者は validuser' OR '1'='1 というような入力を与えることが可能だ。ちなみに password 引数は攻撃には使えない。hashPassword() 関数に渡されハッシュ値の計算に使われることで入力値の検証になっているためだ。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
class Login {
  public Connection getConnection() throws SQLException {
    DriverManager.registerDriver(new
            com.microsoft.sqlserver.jdbc.SQLServerDriver());
    String dbConnection = 
      PropertyManager.getProperty("db.connection");
    // 次のような値を保持する 
    // "jdbc:microsoft:sqlserver://<HOST>:1433,<UID>,<PWD>"
    return DriverManager.getConnection(dbConnection);
  }
  String hashPassword(char[] password) {
    // パスワードのハッシュを作成する
  }
  public void doPrivilegedAction(String username, char[] password)
                                 throws SQLException {
    Connection connection = getConnection();
    if (connection == null) {
      // エラー処理
    }
    try {
      String pwd = hashPassword(password);
      String sqlString = "SELECT * FROM db_user WHERE username = '" 
                         + username +
                         "' AND password = '" + pwd + "'";
      Statement stmt = connection.createStatement();
      ResultSet rs = stmt.executeQuery(sqlString);
      if (!rs.next()) {
        throw new SecurityException(
          "User name or password incorrect"
        );
      }
      // 認証されたので次の処理にすすむ
    } finally {
      try {
        connection.close();
      } catch (SQLException x) {
        // ハンドラへ処理を渡す
      }
    }
  }
}
違反コード (PreparedStatement)
JDBC ライブラリは SQL コマンドを組み立てる API を提供しており、信頼できないデータを無害化してくれる。java.sql.PreparedStatement クラスは入力文字列を適切にエスケープ処理するため、正しく利用すれば SQL インジェクション攻撃を防ぐことができる。このコード例では、doPrivilegedAction() メソッドを変更して java.sql.Statement に代えて PreparedStatement を使っているが、username をサニタイズせずに取り込んでいるため、SQL インジェクションが行われる可能性が残っている。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
class Login {
  public Connection getConnection() throws SQLException {
    DriverManager.registerDriver(new
            com.microsoft.sqlserver.jdbc.SQLServerDriver());
    String dbConnection = 
      PropertyManager.getProperty("db.connection");
    // Can hold some value like
    // "jdbc:microsoft:sqlserver://<HOST>:1433,<UID>,<PWD>"
    return DriverManager.getConnection(dbConnection);
  }
  String hashPassword(char[] password) {
    // Create hash of password
  }
  public void doPrivilegedAction(
    String username, char[] password
  ) throws SQLException {
    Connection connection = getConnection();
    if (connection == null) {
      // エラー処理
    }
    try {
      String pwd = hashPassword(password);
      String sqlString = "select * from db_user where username=" + 
        username + " and password =" + pwd;      
      PreparedStatement stmt = connection.prepareStatement(sqlString);
      ResultSet rs = stmt.executeQuery();
      if (!rs.next()) {
        throw new SecurityException("User name or password incorrect");
      }
     // 認証されたので次の処理にすすむ
    } finally {
      try {
        connection.close();
      } catch (SQLException x) {
        // ハンドラに処理を渡す
      }
    }
  }
}
適合コード (PreparedStatement)
この適合コードではパラメタ化クエリを使っている。パラメタ化クエリでは ? 文字の部分に引数をはめこむことでクエリを構成する。このコードではさらに、username 引数の長さのチェックを行って、勝手な長さのユーザ名を入力されないようにしている。
  public void doPrivilegedAction(
    String username, char[] password
  ) throws SQLException {
    Connection connection = getConnection();
    if (connection == null) {
      // Handle error
    }
    try {
      String pwd = hashPassword(password);
      // Validate username length
      if (username.length() > 8) {
        // Handle error
      }
      String sqlString = 
        "select * from db_user where username=? and password=?";
      PreparedStatement stmt = connection.prepareStatement(sqlString);
      stmt.setString(1, username);
      stmt.setString(2, pwd);
      ResultSet rs = stmt.executeQuery();
      if (!rs.next()) {
        throw new SecurityException("User name or password incorrect");
      }
      // Authenticated; proceed
    } finally {
      try {
        connection.close();
      } catch (SQLException x) {
        // Forward to handler
      }
    }
  }
PreparedStatement クラスの set*() メソッドを使用して強固な型検査を行うこと。set*() メソッドを使うと入力が自動的にダブルクォーテーションでくくられるため、SQL インジェクションの脆弱性低減につながる。プリペアドステートメントはデータベースにデータを挿入するクエリにも使用すること。
リスク評価
ユーザからの入力を無害化せずに処理したり保存したりすると、インジェクション攻撃につながる。
| 
         ルール  | 
      
         深刻度  | 
      
         可能性  | 
      
         修正コスト  | 
      
         優先度  | 
      
         レベル  | 
    
|---|---|---|---|---|---|
| 
         IDS00-J  | 
      
         高  | 
      
         高  | 
      
         中  | 
      
         P18  | 
      
         L1  | 
    
自動検出
| ツール | バージョン | チェッカー | 説明 | 
|---|---|---|---|
| The Checker Framework | 
         2.1.3  | 
      Tainting Checker | Trust and security errors (see Chapter 8) | 
| CodeSonar | 8.1p0 | 
         JAVA.IO.INJ.SQL  | 
      
         SQL injection  | 
    
| Coverity | 7.5 | 
         SQLI  | 
      Implemented | 
| Findbugs | 1.0 | SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE | Implemented | 
| Fortify | 1.0 | 
         HTTP_Response_Splitting  | 
      Implemented | 
| Klocwork | 
         2024.4  | 
      
         SV.DATA.DB  | 
      Implemented | 
| Parasoft Jtest | 2024.2 | CERT.IDS00.TDSQL | Protect against SQL injection | 
| SonarQube | 9.9 | ||
| SpotBugs | 
         4.6.0  | 
      
         SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE  | 
      Implemented | 
関連する脆弱性
CVE-2008-2370 には、Apache Tomcat バージョン4.1.0から4.1.37、5.5.0から5.5.26、6.0.0から6.0.16に影響を与えた脆弱性について記述されている。この脆弱性は、RequestDispatcher が使用されている場合、Tomcat が URI からクエリ文字列を除去する前にパスの正規化を行うというもので、遠隔の攻撃者はリクエスト引数に .. (ドット2つ)を含めることでディレクトリトラバーサル攻撃を行い、任意のファイルを読み取ることができた。
関連ガイドライン
| SEI CERT Perl Coding Standard | IDS33-PL. Sanitize untrusted data passed across a trust boundary | 
| 
         ISO/IEC TR 24772:2013  | 
      
         Injection [RST]  | 
    
| 
         CWE-116, Improper Encoding or Escaping of Output  | 
    
実装の詳細 (Android)
このガイドラインでは、データベース接続の例として Microsoft SQL Server に接続するコード例を示しているが、Android では、データベース接続には SQLite の DatabaseHelper が使われる。Android アプリはネットワーク経由で信頼できないデータを受け取る可能性があるため、このガイドラインは Android アプリ開発にも適用可能である。
参考文献
| 
         [OWASP 2005]  | 
      
         A Guide to Building Secure Web Applications and Web Services  | 
    
| 
         [OWASP 2007]  | 
      |
| [Seacord 2015] | |
| 
         [W3C 2008]  | 
      
         Section 4.4.3, "Included If Validating"  | 
    
翻訳元
これは以下のページを翻訳したものです。
IDS00-J. Prevent SQL injection (revision 211)



