Skip to main content

SQLインジェクション攻撃を防ぐための8つのベストプラクティス:チートシート

2021年3月26日

0 分で読めます

SQLインジェクションは、オンラインアプリケーションにとって最も危険な脆弱性の一つです。ユーザーが信頼できないデータをデータベースクエリに追加すると発生します。たとえば、Webフォームへの入力がこれにあたります。SQLインジェクションが可能な状態では、巧妙な攻撃者が入力を細工して、貴重なデータを盗み出したり、認証を回避したり、データベース内のレコードを改ざんしたりできます。

SQLインジェクション攻撃にはさまざまな種類がありますが、一般に、いずれも原因はよく似ています。ユーザーが入力した信頼できないデータがクエリ文字列と連結されるため、入力によってクエリ本来の意図が変えられてしまう可能性があります。

SQLインジェクションの例をいくつか紹介します。

  • 常に真となる条件をwhere句に追加する。例:' OR 1=1

  • 行コメント--を入力して、クエリの一部を無効にする

  • 最初のクエリを終了させ、新しいクエリを開始する。例:'; DROP TABLE USERS;

  • UNIONを使って複数のテーブルのデータを結合する

このチートシートでは、アプリケーション開発者がSQLインジェクション攻撃を防ぐために活用できる8つのベストプラクティスを紹介します。それでは、アプリケーションをSQLiから守る方法を見ていきましょう。

SQLインジェクション攻撃を防ぐための8つのベストプラクティス(入力値の検証、プリペアドステートメント、ORMのセキュリティ、入力制御など)をまとめたSnykのチートシート。

チートシートをダウンロード

  1. クライアント側の入力検証に頼らない

  2. 権限を制限したデータベースユーザーを使用する

  3. プリペアドステートメントとクエリのパラメーター化を使用する

  4. コードをスキャンしてSQLインジェクションの脆弱性を検出する

  5. ORMレイヤーを使用する

  6. ブロックリストに頼らない

  7. 入力を検証する

  8. ストアドプロシージャの使用には注意する

1. クライアント側の入力検証に頼らない

クライアント側での入力検証は有効です。入力を検証すれば、不正な値がシステムのロジックに送られるのを事前に防げます。しかし、これは残念ながら、悪意がなく、システムを想定どおりに使おうとするユーザーにしか効果がありません。特定の値が無効であることをユーザーにすぐ伝えられるのは、とても便利で使いやすい機能です。そのため、ユーザー体験を向上させる目的でクライアント側の検証を使うべきです。

SQLインジェクション対策としては、クライアント側の検証に頼るべきではありません。ブラウザーに読み込まれたJavaScriptコードを変更すれば、クライアント側の検証は回避できます。また、Postmanなどのツールや昔ながらのcurlコマンドを使って、SQLインジェクションを引き起こすパラメーターを付けたHTTPリクエストをバックエンドに直接送るのも簡単です。

検証はサーバー側で、できるだけ入力元に近い場所で行うべきです。この場合は、SQLクエリを作成する箇所が該当します。クライアントから送信されるものはすべて、潜在的に有害なものとして扱う必要があります。したがって、SQLインジェクション対策としてクライアント側の検証に頼るのは、決して得策ではありません。

2. 権限を制限したデータベースユーザーを使用する

前述のとおり、SQLインジェクション攻撃にはさまざまな種類があり、被害の大きさも異なります。たとえば、SQLクエリが"SELECT * FROM USER WHERE USERID = '" + userid +"'"だとします。" foo' OR '1'='1 "を注入すると、すべてのユーザー情報が取得されるため、すでに深刻な被害につながります。しかし、" '; UPDATE message SET password = 'EVIL"の場合は、侵入者がすべてのエントリを変更するため、さらに大きな問題を引き起こします。

アプリケーション用のデータベースユーザーを作成するときは、そのユーザーに付与する権限を慎重に検討してください。アプリケーションには、すべてのデータベースの読み取り、書き込み、更新が必要でしょうか。テーブルの切り詰めや削除はどうでしょうか。データベース上のアプリケーションの権限を制限すれば、SQLインジェクションによる影響を最小限に抑えられます。アプリケーション用のデータベースユーザーを1つだけにせず、複数のユーザーを作成して、それぞれを特定のアプリケーションロールに割り当てるのが賢明でしょう。セキュリティ上の問題は連鎖して発生することが多いため、深刻な被害を防ぐには、連鎖を構成するすべての要素に注意を払う必要があります。

3. プリペアドステートメントとクエリのパラメーター化を使用する

多くのプログラミング言語には、SQLインジェクションの防止に役立つ機能が組み込まれています。SQLクエリを書くときは、プリペアドステートメントを使ってクエリをコンパイルできます。プリペアドステートメントを使うと、クエリをパラメーター化できます。クエリのパラメーター化とは、SQLステートメントを動的に作成する手法です。プレースホルダーを含むベースクエリを作成し、ユーザーから受け取ったパラメーターを安全にプレースホルダーへ割り当てます。

本物のプリペアドステートメントとパラメーター化クエリを使用すると、エスケープ処理はデータベース自体が行います。まず、プレースホルダーを含むクエリ文字列をもとに、クエリの実行計画が作成されます。次に、(信頼できない)パラメーターがデータベースに送信されます。実行計画はすでに作成されているため、パラメーターによって変更されることはありません。これにより、インジェクションを完全に防止できます。

Javaの例:

String query = "SELECT * FROM USERS WHERE username LIKE ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, parameter);
ResultSet result = statement.executeQuery();

MySqlコネクターを使ったPythonの例:

cursor = conn.cursor(prepared=True)
params = ("foo",)
cursor.execute("SELECT * FROM USERS WHERE username = %s", params)

mysql2を使ったJavaScriptの例:

connection.query("SELECT * FROM USERS WHERE username = ?",[
     req.body.username
    ],function(error, results){}); 
//emulates a prepared statement

//OR

connection.execute("SELECT * FROM USERS WHERE username = ?",[
     req.body.username
    ],function(error, results){});
//prepared statement

たとえばMySqlデータベースを使う場合、JavaScriptで実現する方法はいくつかあります。.query()を使っても、実際のプリペアドステートメントにはならないので注意してください。この場合、パラメーターの置換はクライアント側で処理されます。つまり、プリペアドステートメントをエミュレートしているにすぎません。データベース上で本物のプリペアドステートメントを作成するには、.execute()関数を使用してください。

4. コードをスキャンしてSQLインジェクションの脆弱性を検出する

カスタムコードを書くのは簡単かもしれませんが、ミスは起こりやすいものです。コードを確認するために、コードレビューやペアプログラミングなどのプロセスを導入しているかもしれません。しかし、コードをレビューする人や一緒に作業する人は、セキュリティに詳しいでしょうか。コード内のSQLインジェクションのバグを見つけられるでしょうか。いずれにしても、SQLインジェクションなどのセキュリティ脆弱性がないか、カスタムコードを自動で検査できると便利です。Snyk CodeのようなSAST(静的アプリケーションセキュリティテスト)ツールを使えば、SQLインジェクションなどのセキュリティ脆弱性がないか、コードを自動で検査できます。たとえば、GitリポジトリをSnykに接続すれば、SDLC内で簡単に自動化できます。

MessageRepo.java の executeQuery にサニタイズされていない HTTP 入力が流れ込む様子を示す、Snyk Code の SQL インジェクション画面

5. ORMレイヤーを使用する

オブジェクトリレーショナルマッピング(ORM)レイヤーの利用も検討できます。ORMレイヤーは、データベースのデータをオブジェクトに変換し、その逆も行います。ORMライブラリを使うと、SQLクエリを明示的に記述する必要が減るため、SQLインジェクションのリスクを大幅に低減できます。

既存のORMライブラリの代表例として、Java向けのHibernateやC#向けのEntity Frameworkがあります。これらの言語は型付けが強いため、一般にオブジェクトとデータベーステーブルのマッピングを生成できます。そのため、SQLクエリを自分で扱う必要すらありません。

ただし、カスタムクエリを作成する必要がある場合は問題が生じます。Java向けのHibernateには、独自のHibernate Query Language(HQL)があります。HQLクエリをコンパイルするときも、インジェクションに注意し、プリペアドステートメントと同様に機能するcreateQuery()関数を使ってください。

JavaScriptにも、sequelizeなどのよく知られたORMライブラリがあります。sequelizeを使うと、データベース内の特定の型に値をマッピングする方法を定義できます。しかし、最終的にはORMライブラリがロジックをSQLステートメントに変換する必要があります。つまり、ライブラリがパラメーターを適切にエスケープしていることを信頼することになります。

ORMライブラリにSQLインジェクションの問題がないことを確認するには、既知の脆弱性がないかスキャンしてください。sequelizeやhibernateの古いバージョンや不適切なバージョンを使うと、問題に巻き込まれる可能性があります。Snyk Open Sourceでプロジェクトをチェックすれば、ライブラリに潜むSQLインジェクションや、その他多くの問題を未然に防げます。

6. ブロックリストに頼らない

多くの人にとってすでにおなじみの話だと思いますが、改めてお伝えします。パラメーターにブロックリストを実装しないでください。ブロックリスト方式では、脆弱な入力を定義するルールを集めます。入力がルールに該当すると、リクエストがブロックされます。しかし、ルールが不十分だと悪意のある入力を許してしまい、厳しすぎると正当な入力までブロックしてしまいます。

たとえば、ORという単語を含むリクエストをすべてブロックするとします。妥当なルールに思えるかもしれませんが、実際には「Or」はイスラエルでよくある名前です。そのため、名前を入力する同僚の多くがブロックされてしまいます。単一引用符'も同様です。この文字を含む名前は数えきれないほどあります。O’NeillやO'Donnell、たとえばDont’aのような名前も考えてみてください。

7. 入力を検証する

はい、入力は必ず検証してください。クエリのパラメーター化を伴うプリペアドステートメントはSQLインジェクションに対する最善の防御策ですが、多層防御を常に実践しましょう。データベースユーザーの権限を制限するのと同じように、入力検証はアプリケーション全般のリスクを低減する優れた方法です。また、プリペアドステートメントを利用できない場合もあります。言語がこの仕組みに対応していない場合や、古いデータベースシステムでユーザー入力をパラメーターとして渡せない場合などです。そのようなケースでは、入力検証が有効な代替策になります。

前述のとおり、入力検証ではブロックリストではなく許可リストを使用してください。正規表現などで許可するパターンをすべて定義するか、適切に保守されているライブラリを使いましょう。これをプリペアドステートメントとクエリのパラメーター化と組み合わせれば、堅牢な防御を実現できます。

8. ストアドプロシージャの使用には注意する

ストアドプロシージャを使えばSQLインジェクションを防げると考える人は多くいますが、必ずしもそうとは限りません。アプリケーション内で作成するSQLクエリと同様に、ストアドプロシージャにも悪意あるコードを注入される可能性があります。

アプリケーション内のSQLクエリと同じく、ストアドプロシージャ内のクエリもパラメーター化し、パラメーターを連結しないようにしてください。ストアドプロシージャのSQLインジェクションは、簡単に防止できます。

MySQLでは、次のような書き方はしないでください。

DELIMITER //
CREATE PROCEDURE `FindUsers`(
    IN Username VARCHAR(50)
)
BEGIN

    SET @Statement = CONCAT('SELECT * FROM User WHERE username = ', Username, ' );

    PREPARE stm FROM @Statement;
    EXECUTE stm;

END //
DELIMITER ;

代わりに、ストアドプロシージャではパラメーター化クエリを使用してください。

DELIMITER //
CREATE PROCEDURE `First`(
    IN Username VARCHAR(50)
)
BEGIN

    PREPARE stm FROM 'SELECT * FROM User WHERE username = ?';
    EXECUTE stm USING Username;

END //
DELIMITER ;

上記のとおり、ストアドプロシージャにもアプリケーションコードと同じルールが適用されます。ストアドプロシージャの実装方法はデータベースによって異なります。使用するデータベースでの実装方法を把握し、インジェクションにも注意してください。すべてのロジックをアプリケーション内に置くほうがよいと私は考えますが、開発に使う言語でプリペアドステートメントを利用できない場合は、ストアドプロシージャが妥当な解決策になることもあります。

続きを読む

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

Blog

AIコーディングエージェントが不適切なアクセス制御を繰り返し実装する理由

AIコーディングエージェントは、コンパイルが通りレビューも通過する一方で、あるテナントのデータを別のテナントに公開してしまう認可ロジックを生成することがあります。不適切なアクセス制御が検出しにくい理由と、その防止策をご紹介します。