Rails SQLインジェクション


バックエンド開発を研究するとき、私はSQL(および他のデータベース問い合わせ言語)を使用することが直接コードをはるかに速くするということを学びました.SQLは特にクエリを行うために構築されているので、それは確かに意味がありますが、私は実際にこれが実際にされている場合、実際に確認したことがない、それがされている場合、それはどのくらいの速さです.それで、私はテストにそれを置くことに決めました、そして、Ruby ActiveRecordベースのアプローチよりむしろ直接SQL注射を使用するならば、私のコードがより速く走らせるという仮説を検証するために、Ruby on Railsのミニプロジェクトを作成しました.何が起こったのです.

セットアップ
まず、テストシナリオを作成し、SQLとActiveRecordの実装を実装する必要がありました.私はプロジェクトを作成するための手順を行くつもりはないが、ここではハイライトです
  • ユーザーと関連する注文は、データベースに格納されます
  • 命令には、payerRange名と属性名
  • Orderモデルでは、指定されたユーザーに関連付けられた順序を抽出し、CreateDRADER
  • 必要に応じて、メソッドを呼び出すときにpayerRage名を指定して結果をフィルタリングできます
  • 私のデモユーザーは10関連付けられている
  • これらの命令のうちの6つは、「Store 1」に等しいpayerRange名を持ちます
  • 私のアプリとデータベースを設定した後、私は上記の箇条書きに記載されているメソッドをコード化する必要があります.

    通常の実装( Ruby & ActiveRecord )
    ここでは、このコードが直接SQLを注入せずにどのように見えるかを示します.
    def self.sort_orders(user_id, payer_name = '*')
      if payer_name == '*'
        User.find(user_id).orders.sort {|a, b| a.created_at <=> b.created_at}
      else
        User.find(user_id).orders.filter{|t| t.payer_name == payer_name}.sort {|a, b| a.created_at <=> b.created_at}
      end
    end
    
    したがって、PayerRange名が指定されているかどうかに関係なく、コードは、渡されたuserRound IDに基づいてユーザーインスタンスを見つけることによって開始します.PayerRage名が指定された場合、結果はPayerRange名が提供されたものと一致する結果を含めるためにこの時点でフィルタリングされます.最後に、任意の残りの結果をcreatedRAGEで昇順でソートされます.
    PayerRound名を指定すると、ソート前に結果をフィルター処理します.これは私のパフォーマンスを少し助け、テストをより公正にします.

    SQLインジェクション実装
    さて、まったく同じことをする方法を追加したいのですが、今回はRubyとActiveRecordに頼らずにSQLで結果を取得します.
    def self.sort_orders_SQL(user_id, payer_name = '*')
      if payer_name == '*'
        self.find_by_sql(["SELECT * FROM orders WHERE user_id = ? ORDER BY created_at ASC", user_id].flatten)
      else
        self.find_by_sql(["SELECT * FROM orders WHERE user_id = ? AND payer_name = ? ORDER BY created_at ASC", user_id, payer_name].flatten)
      end
    end
    
    前述のように、これは全く同じことをします.つの主要な利点;しかし、SQLクエリで順序ロジックを追加することができます.payerRange名が指定されている場合、フィルタロジックに対する同じ取引.これは、データベースクエリを注入するための優れたユースケースです!

    テスト
    今、私が準備をしているので、テストしましょう.このため、Rubyの宝石を使用しますbenchmark それぞれのメソッドを実行するのにかかる時間を計算する.これらは巨大な方法ではないので、私は結果をより簡単に操作することができるように何度もそれらを実行します.を使用しますbm 両方のテストを同時に実行する方法.私は各方法を1000回実行することから始めます、そして、楽しみのために、私は5000と10000の繰り返しで同じテスト方法を走らせます.

    ' payerRange 'フィルタのないテスト
    Benchmark.bm do |benchmark|
      benchmark.report("no-inject") do  // label denoting no SQL injection
        1000.times do
          Order.sort_orders(User.first.id)
        end
      end
    
      benchmark.report("injectSQL") do  // label denoting SQL injection used
        1000.times do
          Order.sort_orders_SQL(User.first.id)
        end
      end
    end
    
    テストを1000回ごとに以下のデータで実行します(注:縮小カラムは計算され、ベンチマークのGEMによって提供されません)
    システムCPU時間
    ユーザCPU時間
    合計CPU時間
    注入しない
    0.421875
    1.609375
    2.03125
    インジェクション
    0.203125
    0.5
    0.703125
    削減
    51.85 %
    68.93 %
    65.38 %
    5000回繰り返した結果、次のデータが得られた.
    システムCPU時間
    ユーザCPU時間
    合計CPU時間
    注入しない
    0.828125
    3.21875
    4.046875
    インジェクション
    0.6875
    2.078125
    2.765625
    削減
    16.98 %
    35.44 %
    31.66 %
    最終的に10000回の繰り返しにより、以下のデータが得られた.
    システムCPU時間
    ユーザCPU時間
    合計CPU時間
    注入しない
    1.890625
    6.234375
    8.125000
    インジェクション
    1.156250
    2.625000
    3.781250
    削減
    38.84 %
    57.89 %
    53.46 %

    PayerRange名フィルタによるテスト
    さて、PayerRange名がstore 1である場合にのみ、この値を返す必要条件を追加します.
    Benchmark.bm do |benchmark|
      benchmark.report("no-inject") do  // label denoting no SQL injection
        1000.times do
          Order.sort_orders(User.first.id, "Store1")
        end
      end
    
      benchmark.report("injectSQL") do  // label denoting SQL injection used
        1000.times do
          Order.sort_orders_SQL(User.first.id, "Store1")
        end
      end
    end
    
    以下のデータでテスト結果を1000回実行します
    システムCPU時間
    ユーザCPU時間
    合計CPU時間
    注入しない
    0.390625
    0.9375
    1.328125
    インジェクション
    0.09375
    0.203125
    0.296875
    削減
    76.00 %
    78.33 %
    77.65 %
    5000回繰り返した結果、次のデータが得られた.
    システムCPU時間
    ユーザCPU時間
    合計CPU時間
    注入しない
    1.234375
    3.59375
    4.828125
    インジェクション
    1.0625
    1.3125
    2.375
    削減
    13.92 %
    63.48 %
    50.81 %
    最終的に10000回の繰り返しにより、以下のデータが得られた.
    システムCPU時間
    ユーザCPU時間
    合計CPU時間
    注入しない
    3.28125
    11.359375
    14.640625
    インジェクション
    1.96875
    4.203125
    6.171875
    削減
    40.00 %
    63.00 %
    57.84 %

    結果
    このデータを使用すると、直接SQL注入がRuby & ActiveRecordより高速に動作するという仮説は、実際には、サポートされています.また、縮小図を見るとき、彼らはそれが実際に非常に高速だと示唆しています!プログラムが私のコンピュータで何を実行しているかを含む結果に影響を及ぼす他の要因があることを心に留めておくことは重要です、それで、2回同じテストを実行することは同じ結果を得ることが非常にありそうもありません.しかし非フィルタリングのための50.17 %の平均減少とフィルタリングされた結果のためのwhopping 62.10 %で、このSQL注入戦略が正しい状況で巨大なパフォーマンス後押しであるという疑問が、ありません.

    ボーナス
    上の私の分析について私を悩ましている何かが、ありました.SQLの戦略は、巨大なパフォーマンスのブーストだったが、それは確かにエッジを与えるイテレータメソッドをスキップするの利点があります.純粋なSQL対ActiveRecordの効果を測定するには、その調停因子を切り出す必要があります.そこで、同じテストの結果のテーブルですが、すべての並べ替えとフィルタリングを削除しました.これは単にuserCirus IDに基づいてユーザーを見つけ、特定の順序ですべての関連付けられた順序を取得します.
    私は5000回の繰り返しでテストをした.
    システムCPU時間
    ユーザCPU時間
    合計CPU時間
    注入しない
    1.96875
    4.28125
    6.25
    インジェクション
    1.171875
    2.546875
    3.71875
    削減
    40.48 %
    40.51 %
    40.50 %
    したがって、これはActiveRecordの上でSQLを直接使用することから、まだ大きい、少しより小さい、パフォーマンス利得があることを示唆します.仮説はまだ支持