Skip to content

Commit 807786d

Browse files
committed
Adding tests and code to ensure that adapter is not vulnerable to limit and offset SQL injection.
1 parent bfb239a commit 807786d

File tree

4 files changed

+99
-4
lines changed

4 files changed

+99
-4
lines changed

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,12 +386,32 @@ def quote_column_name(name)
386386
end
387387

388388
def add_limit_offset!(sql, options)
389+
if options[:offset]
390+
raise ArgumentError, "offset should have a limit" unless options[:limit]
391+
unless options[:offset].kind_of?Integer
392+
if options[:offset] =~ /^\d+$/
393+
options[:offset] = options[:offset].to_i
394+
else
395+
raise ArgumentError, "offset should be an integer"
396+
end
397+
end
398+
end
399+
400+
unless options[:limit].kind_of?Integer
401+
# is it just a string which should be an integer?
402+
if options[:limit] =~ /^\d+$/
403+
options[:limit] = options[:limit].to_i
404+
else
405+
raise ArgumentError, "limit should be an integer"
406+
end
407+
end
408+
389409
if options[:limit] and options[:offset]
390410
total_rows = @connection.select_all("SELECT count(*) as TotalRows from (#{sql.gsub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally")[0][:TotalRows].to_i
391411
if (options[:limit] + options[:offset]) >= total_rows
392412
options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
393413
end
394-
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]} ")
414+
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]}")
395415
sql << ") AS tmp1"
396416
if options[:order]
397417
order = options[:order].split(',').map do |field|
@@ -410,7 +430,7 @@ def add_limit_offset!(sql, options)
410430
end.join(', ')
411431
sql << " ORDER BY #{change_order_direction(order)}) AS tmp2 ORDER BY #{order}"
412432
else
413-
sql << " ) AS tmp2"
433+
sql << ") AS tmp2"
414434
end
415435
elsif sql !~ /^\s*SELECT (@@|COUNT\()/i
416436
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) do

test/cases/aaaa_create_tables_test_sqlserver.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class AAAACreateTablesTestSqlserver < ActiveRecord::TestCase
55
self.use_transactional_fixtures = false
66

77
def setup
8-
@ar_path = "#{File.dirname(__FILE__)}/../schema"
8+
@ar_path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'rails/activerecord/test/schema'))
99
@base_path = "#{File.dirname(__FILE__)}/../fixtures/db_definitions"
1010
end
1111

test/cases/adapter_test_sqlserver.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
require "cases/helper"
1+
require 'cases/helper'
22
require 'models/default'
33
require 'models/post'
44
require 'models/task'
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
require 'cases/helper'
2+
# require File.dirname(__FILE__) + '/../../lib/shoulda'
3+
require 'rubygems'
4+
require 'shoulda'
5+
6+
class OffsetAndLimitTest < ActiveRecord::TestCase
7+
def setup
8+
@connection = ActiveRecord::Base.connection
9+
end
10+
11+
context "selecting with limit" do
12+
setup do
13+
@select_sql = 'SELECT * FROM schema'
14+
end
15+
16+
should "alter SQL to limit number of records returned" do
17+
options = { :limit => 10 }
18+
assert_equal('SELECT TOP 10 * FROM schema', @connection.add_limit_offset!(@select_sql, options))
19+
end
20+
21+
should "only allow integers for limit" do
22+
options = { :limit => 'ten' }
23+
assert_raise(ArgumentError) {@connection.add_limit_offset!(@select_sql, options) }
24+
end
25+
26+
should "convert strings which look like integers to integers" do
27+
options = { :limit => '42' }
28+
assert_nothing_raised(ArgumentError) {@connection.add_limit_offset!(@select_sql, options)}
29+
end
30+
31+
should "not allow sql injection via limit" do
32+
options = { :limit => '1 * FROM schema; DELETE * FROM table; SELECT TOP 10 *'}
33+
assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options) }
34+
end
35+
end
36+
37+
context "selecting with limit and offset" do
38+
setup do
39+
# we have to use a real table as we need the counts
40+
@select_sql = 'SELECT * FROM accounts'
41+
class Account < ActiveRecord::Base; end
42+
43+
# create 10 Accounts
44+
(1..10).each {|i| Account.create!}
45+
end
46+
47+
should "have limit if offset is passed" do
48+
options = { :offset => 1 }
49+
assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options) }
50+
end
51+
52+
should "only allow integers for offset" do
53+
options = { :limit => 10, :offset => 'five' }
54+
assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options)}
55+
end
56+
57+
should "convert strings which look like integers to integers" do
58+
options = { :limit => 10, :offset => '5' }
59+
assert_nothing_raised(ArgumentError) {@connection.add_limit_offset!(@select_sql, options)}
60+
end
61+
62+
should "alter SQL to limit number of records returned offset by specified amount" do
63+
options = { :limit => 3, :offset => 5 }
64+
expected_sql = %&SELECT * FROM (SELECT TOP 3 * FROM (SELECT TOP 8 * FROM accounts) AS tmp1) AS tmp2&
65+
assert_equal(expected_sql, @connection.add_limit_offset!(@select_sql, options))
66+
end
67+
68+
# Not really sure what an offset sql injection might look like
69+
should "not allow sql injection via offset" do
70+
options = { :limit => 10, :offset => '1 * FROM schema; DELETE * FROM table; SELECT TOP 10 *'}
71+
assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options) }
72+
end
73+
end
74+
75+
end

0 commit comments

Comments
 (0)