SQL Injectionは、攻撃者がクエリパラメータに悪意のあるSQLコードを注入し、データへの不正アクセスやデータの改変を可能にするデータベース攻撃の一種です。OWASP(2025年)によると、SQL Injectionはデータベース全体を危険にさらす可能性のある重大な脆弱性の一つであり続けています。SQLコードインジェクションにより、攻撃者はレコードの読み取り、変更、削除が可能になり、場合によってはサーバーのオペレーティングシステムへのアクセスも可能になります。
重要ポイント
SQL Injectionは、アプリケーションがユーザーデータとの文字列連結によってSQLクエリを構築する際に発生する脆弱性です。攻撃者は、特別に細工された文字列をクエリパラメータに渡すことでSQLコマンドの構造を変更します。注入された文字列はデータではなくSQLコードの一部となり、攻撃者はデータベースに対して任意のクエリを実行できるようになります。Verizonデータ漏洩レポート(2025年)によると、SQL Injectionは調査されたすべてのデータ漏洩の8%に存在します。
よく知られた脆弱性であるにもかかわらず(1990年代後半に初めて言及)、SQL Injectionは現代のアプリケーションでも依然として発見されています。その理由は人為的エラーです。開発者は文字列連結を使用したコードを作成し、レガシーコードはリファクタリングされず、ORMフレームワークは不適切に使用されます(例:文字列補間を使用したrawクエリ)。Veracode(2026年)によると、スキャンされた全アプリケーションの約14%に少なくとも1つのSQLi脆弱性が含まれています。
SQL Injectionが成功すると、攻撃者は幅広い能力を手に入れます。パスワードハッシュやユーザーの個人データを含むあらゆるデータベーステーブルの読み取り、レコードの変更や削除、管理操作(DROP TABLE、TRUNCATE)の実行、一部の構成ではxp_cmdshell(MSSQL)やINTO OUTFILE(MySQL)を介したリモートコマンド実行が可能になります。結果はユーザーデータの漏洩からシステムの完全な制御喪失まで及びます。
SQLインジェクションは、データベースからデータを抽出する方法によって分類されます。方法の選択は、アプリケーションがクエリ結果とエラーメッセージをどのように処理するかに依存します。OWASP分類では、3つの主要なタイプが特定されています。In-band(同じチャネルを介したデータ抽出)、Inferential/Blind(論理的推論)、Out-of-band(別のチャネルを介したデータ転送)です。
| タイプ | データ抽出方法 | 複雑さ | 頻度 |
|---|---|---|---|
| In-band(古典的) | クエリ結果を直接利用 | 低 | 高 |
| Blind SQLi | サーバー応答からの論理的推論 | 高 | 中 |
| Out-of-band | 外部チャネル(DNS、HTTP)経由 | 中 | 低 |
最も一般的なタイプです。攻撃者はクエリパラメータにSQLコードを注入し、その結果がサーバーの応答に直接表示されます。Error-based(DBエラーメッセージ経由)とUNION-based(UNION SELECT演算子経由)の2つのサブタイプがあります。Error-basedは、テーブル名やクエリ構造を明らかにするMySQLの構文エラーなど、エラーメッセージからの情報を利用します。UNION-basedは、正当なクエリの結果をデータベースの他のテーブルのデータと結合することを可能にします。
アプリケーションがクエリ結果やエラーメッセージを表示しない場合に使用されます。攻撃者は論理条件を含むクエリを送信し、サーバーの応答の違い(応答時間やページ内容など)を分析することではい/いいえの質問を行います。Time-based Blind SQLiは、条件を確認するために遅延関数(SLEEP、WAITFOR DELAY)を使用します。条件が真の場合、ページの読み込みに時間がかかります。この方法は非常に低速で、1つのレコードを抽出するのに数時間かかることがあります。
# Blind SQL Injectionの例(時間ベース)
# SQLiに脆弱な場合、条件が満たされるとSLEEP(2)が実行される
import requests
import time
payload = "' OR IF(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a',SLEEP(2),0) -- "
url = "http://example.com/user?id=1" + payload
start = time.time()
response = requests.get(url)
elapsed = time.time() - start
# 応答が>2秒後に届いた場合 — パスワードの最初の文字は'a'
print(f"Time: {elapsed:.2f}s - First letter is 'a'" if elapsed > 2 else "First letter is not 'a'")
データはHTTP応答ではなく、代替チャネル(DNSクエリ、外部サーバーへのHTTPリクエスト、SMTP)を介して送信されます。アプリケーションがクエリ結果を返さず、エラーも表示しない場合に使用されます。MySQLはDNSクエリをトリガーできるLOAD_FILE()関数をサポートしており、MSSQLにはリモートSMBサーバーにデータを送信するxp_dirtreeがあります。Out-of-band SQL Injectionは効果的ですが、攻撃者側の追加サーバー設定と特定のDB関数が必要です。
SQL Injectionのメカニズムは、SQLが文字列リテラルに引用符を使用することに基づいています。アプリケーションがユーザー入力をエスケープせずにSQLクエリに直接挿入する場合、攻撃者は文字列を“閉じて”任意のSQLコードを追加できます。例えば、クエリSELECT * FROM users WHERE name = '$input'に' OR '1'='1を挿入すると、SELECT * FROM users WHERE name = '' OR '1'='1'に変換され、すべてのユーザーが返されます。
クエリSELECT * FROM users WHERE username = '$user' AND password = '$pass'を使用するログインフォームを考えます。攻撃者がユーザー名フィールドにadmin' -- を入力し、パスワードを空のままにすると、結果のクエリはSELECT * FROM users WHERE username = 'admin' -- ' AND password = ''になります。--文字は残りのクエリをコメントアウトし、パスワードチェックを無効にします。サーバーはadminユーザーのレコードを返し、攻撃者はパスワードを知らなくてもログインできます。
# SQL Injectionの例 — 認証バイパス
# 脆弱なコード:直接の文字列連結
def login_vulnerable(username, password):
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
cursor.execute(query)
return cursor.fetchone() is not None
# username = "admin' --" がパスワードチェックを無効化
# 安全なバージョン:パラメータ化クエリ
def login_secure(username, password):
query = "SELECT * FROM users WHERE username = %s AND password = %s"
cursor.execute(query, (username, password))
return cursor.fetchone() is not None
UNION SELECT演算子は、2つのSELECTクエリの結果を結合できます。攻撃者が脆弱なパラメータを見つけた場合、別のテーブルからのクエリとともにUNION SELECTを追加します。例:' UNION SELECT username, password FROM admins --。成功の条件は、両方のクエリのカラム数が一致することです。カラム数はORDER BYによって決定されます(' ORDER BY 1--、次に2、3...エラーが発生するまで)。カラム数がわかれば、攻撃者は同じフィールド数のUNION SELECTを挿入します。
モバイルアプリケーションは、サーバーサイド(API)とクライアントサイド(ローカルデータベース:SQLite、Realm)の両方でSQL Injectionに直面します。モバイルAPIでのサーバーサイドSQLiはWebアプリケーションと同様ですが、ローカルデータベースは追加の攻撃ベクトルを生み出します。アプリケーションがSQLiteにデータを保存し、文字列連結でクエリを実行する場合、APIを介してローカルデータベースに入力された悪意のあるデータが後続の処理でSQLiを引き起こす可能性があります。
デバイス上のローカルSQLiteデータベースも、アプリケーションが文字列連結でクエリを構築する場合、SQL Injectionに対して脆弱です。AndroidのContent ProvidersとiOSのCore Dataはデフォルトでパラメータ化を使用しますが、rawクエリには開発者の注意が必要です。SQLiteはセミコロンで区切られた複数クエリをサポートしていないため、攻撃者の選択肢は制限されますが、WHERE条件によるデータ読み取りは防げません。AndroidではselectionArgs、iOSではパラメータ付きのNSPredicateを常に使用してください。
モバイルアプリケーションが通信するAPIは、他のWebサーバーと同様に脆弱です。モバイル開発者はSQLiをバックエンドだけの問題と考えることがよくありますが、脆弱性はクライアントからパラメータを受け入れるAPIエンドポイントで発生します。責任の分離は保護にはなりません。バックエンド開発者がクエリのパラメータ化を忘れた場合、ユーザーのモバイルアプリが攻撃ベクトルになります。バックエンドにORMまたはプリペアドステートメントの使用を要求してください。
// AndroidのローカルSQLiteにおけるSQL Injectionの例
// 脆弱なコード:直接連結
fun getUserVulnerable(db: SQLiteDatabase, userId: String): Cursor {
return db.rawQuery(
"SELECT * FROM users WHERE id = $userId", null
)
}
// 安全なバージョン:selectionArgsによるパラメータ化
fun getUserSecure(db: SQLiteDatabase, userId: String): Cursor {
return db.rawQuery(
"SELECT * FROM users WHERE id = ?",
arrayOf(userId)
)
}
SQL Injectionからの保護は単純な原則に基づいています。SQLクエリでユーザー入力を決して信頼しないことです。唯一信頼できる方法は、SQLコードとデータを別々に渡すクエリパラメータ化(プリペアドステートメント)です。エスケープ、検証、WAFなどの他のすべての方法は追加のセキュリティレイヤーですが、パラメータ化の代わりにはなりません。OWASP(2025年)によると、パラメータ化はSQL Injection攻撃の100%を防ぎます。
プリペアドステートメントを使用する場合、SQLクエリは最初にDBサーバーによってデータなしでコンパイルされ、その後パラメータ値が別々に渡されます。データベースはパラメータを実行可能コードではなくデータとして扱います。攻撃者が' OR '1'='1を渡しても、データベースはそれをSQLコードではなく文字列値として解釈します。プリペアドステートメントは、PHPのPDO、JavaのPreparedStatement、Pythonのcursor.executeなど、すべての最新言語とフレームワークでサポートされています。
最新のORM(Hibernate、Entity Framework、SQLAlchemy、Room)は、開発者がrawクエリに切り替えない限り、クエリ実行時に自動的にパラメータ化を使用します。ただし、ORMは完全には保護しません。JPAの@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)のような構造では、連結ではなく名前付きパラメータを介してパラメータを渡す必要があります。クエリビルダー(Knex、jOOQ)も、rawメソッドを使用しない場合はデフォルトでクエリをパラメータ化します。
特殊文字のエスケープ(mysql_real_escape_string)は、すべてのタイプのSQL Injectionから保護しない時代遅れの方法です。問題は、エスケープがエンコーディングに依存し、マルチバイトエンコーディング(アジアのシステムでのGBKなど)を使用するとバイパスされる可能性があることです。エスケープは、パラメータ化が不可能なレガシーコードでのみ、かつ常に厳格な入力タイプ検証と組み合わせて使用してください。
| 方法 | 効果 | 推奨 |
|---|---|---|
| プリペアドステートメント | 100% | すべてのクエリで必須 |
| ORM(正しい使用法) | 99% | 推奨 |
| 文字列エスケープ | 70%(エンコーディングに依存) | レガシーのみ |
| 入力検証(ホワイトリスト) | 50%(数値のみ) | 追加 |
| WAF(Web Application Firewall) | 60% | 追加 |
アプリケーションアカウントには必要最小限の権限(SELECT、INSERT、UPDATE、DELETE — アプリケーションが実際に必要とするテーブルのみ)を付与する必要があります。アプリケーションアカウントでのDROP、TRUNCATE、CREATEの使用を禁止します。これにより、SQL Injectionが成功した場合でも被害を限定できます。攻撃者はテーブルを削除したり管理操作を実行したりできなくなります。
定期的なSQL Injectionテストはセキュア開発パイプラインの一部であるべきです。静的解析、動的スキャン、手動ペネトレーションテストの組み合わせが最良の結果をもたらします。Synopsysサイバーセキュリティレポート(2025年)によると、自動スキャナーはSQLi脆弱性の最大70%を発見しますが、複雑なBlind攻撃には手動テストが必要です。
モバイルアプリケーションの場合、ローカルSQLiteの解析も重要です。すべてのrawQuery呼び出し、ContentProviderクエリ、rawQueryを使用したRoomクエリを確認してください。ツール:Android Studio Lint(SQLiteのSQLiを検出)、MobSF(Mobile Security Framework)によるAPK/IPAの自動静的・動的解析。また、モバイルアプリケーションのトラフィックをプロキシインターセプトしながらsqlmapを使用してAPIエンドポイントをテストすることをお勧めします。
よくある質問
SQL InjectionはSQLクエリを介してリレーショナルデータベースを攻撃します。NoSQL Injectionはクエリ演算子($gte、$ne、$where)を介して非リレーショナルデータベース(MongoDB、Couchbase)に影響を与えます。MongoDBでは、アプリケーションがJSON文字列からBSONドキュメントを構築する場合にインジェクションが可能です。保護メカニズムは同様です:パラメータ化とタイプ検証です。
ユーザーデータとの文字列連結によってSQLクエリが形成されているすべての場所を見つけてください。"SELECT ... WHERE id = " + userIdやf"UPDATE ... SET name = '{name}'"のようなパターンを探します。そのような行はすべて潜在的なSQLインジェクションです。すべてをパラメータ化クエリまたはプリペアドステートメントに置き換えてください。
ORMフレームワークは、クエリビルダーメソッドと名前付きパラメータを使用する場合にのみ自動的に保護します。rawクエリ(JPAのnativeQuery、RoomのrawQuery)を使用する場合、保護は機能しません。文字列連結ではなく、準備された式を介してパラメータを渡す必要があります。
セカンドオーダーSQL Injectionは、悪意のあるデータがデータベースに安全に保存され、その後エスケープなしで別のクエリで使用される攻撃です。例えば、攻撃者が' OR '1'='1のようなユーザー名で登録します。データは文字列として保存され、登録時には攻撃は発生しません。しかし、別のクエリがパラメータ化なしでSQLでそのユーザー名を使用すると、インジェクションが発動します。
NoSQL Injectionは、開発者の認識不足により潜在的に危険な可能性があります。開発者はSQL Injectionについて知っており、ほとんどの開発者がORMを使用していますが、NoSQL Injectionについて知っている人はほとんどいません。MongoDBでは、不適切に形成されたクエリがコレクション内のすべてのドキュメントを返す可能性があります。保護は同様です。プリペアドステートメント(BSONパラメータ化)と厳格な入力検証です。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。