SQLインジェクションとは?起きるパターンと対策法

SQLインジェクションとは?起きるパターンと対策法

#セキュリティ#Web開発#脆弱性対策

SQLインジェクションは古くから知られながら、いまも情報漏洩の主要な原因であり続けている脆弱性です。 対策はほぼ確立されていて、プレースホルダ(パラメータ化クエリ)を使えているかどうかでほぼ決まります。

SQLインジェクションとは

SQLインジェクション:ユーザー入力を使ってSQL文を組み立てる箇所に攻撃者が細工した文字列を送り込み、開発者が意図しないSQLを実行させる脆弱性です。 文字列連結でSQLを組む設計が根本原因になります。

攻撃が成立すると、' OR '1'='1 のような入力による認証回避、テーブル全体を対象にした情報漏洩、UPDATEやDELETEを混ぜ込んだデータの改ざんや削除といった被害につながります。 データベースには個人情報や認証情報が集まるため、1箇所の穴が全社的な漏洩に直結します。 UNIONで別テーブルの内容を結果に混ぜ込む、条件の真偽で1文字ずつ抽出するなど手口は幅広く、攻撃を自動化するツールも普及しています。

SQLインジェクションが起きるパターン

典型的な混入箇所は次のとおりです。

  • ユーザー入力を文字列連結やテンプレート展開でSQLに埋め込んでいる("SELECT * FROM users WHERE name = '" + input + "'"
  • ORMを使っていても、生SQLを渡すAPI(Railsのwhere("... = #{params}")find_by_sqlなど)に入力を直接差し込んでいる
  • ORDER BYのカラム名やLIMITの件数など、プレースホルダを使えない箇所に入力をそのまま渡している
  • 画面にエラーや結果が出ないブラインドSQLiで、応答時間や真偽の差分から1文字ずつ情報を抜かれる
  • 入力を一度エスケープしたつもりでも、多重デコードや文字コードの扱いで無効化されている

共通するのは「入力がデータではなくSQLの構文として解釈される経路がひとつでもあれば成立する」という点です。

対策法

原則は「入力をSQLの文字列に混ぜず、プレースホルダでデータとして渡す」ことです。

  • プレースホルダ(パラメータ化クエリ、バインド機構)を使い、値とSQL構文を分離する。これが本命の対策になる
  • ORMのクエリビルダを標準経路として使い、生SQLを渡すAPIは使用箇所を洗い出してレビューで管理する
  • カラム名や並び順などプレースホルダを使えない箇所は、入力を許可リストの固定値に写像してから使う
  • アプリが使うDBユーザーの権限を最小化し、不要なテーブルへのアクセスや管理者権限を与えない(被害軽減策)
  • 詳細なSQLエラーを利用者に返さず、攻撃者にスキーマの手がかりを与えない

入力値検証は補助であり、本命はプレースホルダによる構文とデータの分離です。 既存コードでは生SQLと文字列連結の箇所を先に洗い出し、そこから順に潰すのが費用対効果の高い進め方になります。

Rails での事例

Active Record を普通に使っているかぎり、値は自動でエスケープされSQLインジェクションは起きません。 穴が開くのは、条件を文字列で組み立て、そこに入力を式展開したときです。

# 危険:params[:email] が SQL の構文として解釈される
User.where("email = '#{params[:email]}'")

' OR '1'='1 を渡されれば条件が常に真になり、'; DROP ... の類も通ります。 直し方は、プレースホルダに値を渡すことです。

# ? のプレースホルダ、または名前付きプレースホルダ
User.where("email = ?", params[:email])
User.where(email: params[:email])   # ハッシュ条件が最も安全で読みやすい

見落とされやすいのが、プレースホルダを使えない箇所です。 order に渡すカラム名や並び順は値ではないため、? で縛れません。 ここに入力を直接渡すと、カラム名経由のインジェクションになります。

# 危険:ソートキーをそのまま order に渡している
Post.order(params[:sort])

カラム名やソート方向は、許可リストの固定値へ写像してから使います。

SORT_COLUMNS = { "new" => "created_at", "title" => "title" }.freeze

column = SORT_COLUMNS.fetch(params[:sort], "created_at")
Post.order(column => :asc)

find_by_sqlexecute で生SQLを書く箇所も同じ原則です。 文字列連結をやめ、バインド変数(sanitize_sql_array?)を通します。

この記事をシェア