safe_query 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE +21 -0
  4. data/README.md +92 -0
  5. data/lib/safe_query.rb +47 -0
  6. metadata +115 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c32a8798dd6eb9c24383f4857c3f1d75f8bbc37119a307dfa038f0e9b121455a
4
+ data.tar.gz: 27bbc0fc2ae83ab9c38c7448f3a174eb430a6fe156cc9f5d9cf6e40acb8235a9
5
+ SHA512:
6
+ metadata.gz: 40ce4b878b2a67c199e46128ad73aac5bcba1def0e54740cfe076421ebf31852d88bc4b0454100cd55339a5fa157e7cdf57b1c8fa3235a9b3b9ba963e1699187
7
+ data.tar.gz: 983393fd6fc71ec8bb1dd4ff6565eb423c43652372466be0148fb85c74c8c14b7dad82e5b0d3de8e2aa3e10a175421a1c7939945b93fb33c94f0402db917851d
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # SafeQuery changelog
2
+
3
+ ## Unreleased
4
+ - Add your PR changelog line here
5
+
6
+ ## 0.1.0 (2023-03-22)
7
+ - Initial release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Peter Cai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # SafeQuery
2
+
3
+ Safely query stuff in ActiveRecord.
4
+
5
+ ## Why
6
+
7
+ To prevent unbounded resource consumption, we always want to limit how many rows can be returned from database fetches.
8
+
9
+ Calls to `ActiveRecord::Relation#each` (without a LIMIT clause) are dangerous and should be avoided, because they can accidentally trigger an
10
+ unpaginated database fetch for millions of rows, which can exhaust your web server or database resources. Exceptions to this rule should be carefully vetted.
11
+
12
+ Worse, it's common to hit this problem only in production, because development environments seldom contain enough database rows to highlight the issue.
13
+ This makes it easy to write code that seems to work well, but fails when operating on a database with more data.
14
+
15
+ This gem raises an exception whenever you attempt to call `ActiveRecord::Relation#each`, giving you the opportunity to catch and fix
16
+ this before any unsafe code hits production.
17
+
18
+ ## How it works
19
+
20
+ With this gem installed, Rails will throw an exception when you make an unsafe query.
21
+
22
+ ## Compatibility:
23
+
24
+ - Rails 5+
25
+ - Ruby 2.6+
26
+ - Postgres, MySQL, SQLite, maybe others (untested)
27
+
28
+ ## Installation:
29
+
30
+ Add to your gemfile:
31
+
32
+ ```
33
+ gem 'safe_query', group: [:development, :test]
34
+ ```
35
+
36
+ then `bundle install`.
37
+
38
+ It's recommended to set `config.active_record.warn_on_records_fetched_greater_than` (available since Rails 5), so you have warnings
39
+ whenever a query is returning more rows than expected, even when using this gem.
40
+ For example, if your app is never supposed to have no more than 100 records per page, add to `config/environments/development.rb`:
41
+
42
+ ```ruby
43
+ config.active_record.warn_on_records_fetched_greater_than = 100
44
+ ```
45
+
46
+ ## Example fixes:
47
+
48
+ ### Use `find_each` instead
49
+
50
+ Sometimes the fix is as easy as changing
51
+
52
+ ```ruby
53
+ book.authors.each do |author|
54
+ ```
55
+
56
+ to this:
57
+
58
+ ```ruby
59
+ book.authors.find_each do |author|
60
+ ```
61
+
62
+ Sometimes this doesn't work:
63
+ - For some reason you don't have an autoincrementing primary key ID for Rails to paginate on
64
+ - You have a specific sort order and you want to maintain the sort order
65
+
66
+ In those cases, you may have to add some custom code to maintain your existing app behavior. But otherwise, you can
67
+ use `find_each` or any other solution from the [ActiveRecord::Batches](https://api.rubyonrails.org/classes/ActiveRecord/Batches.html) API.
68
+
69
+ ### Paginate your results
70
+
71
+ Use your existing pagination solution, or look at adding [pagy](https://github.com/ddnexus/pagy), [kaminari](https://github.com/kaminari/kaminari),
72
+ [will_paginate](https://github.com/mislav/will_paginate), etc to your app.
73
+
74
+ ### Just add a `limit` clause
75
+
76
+ Sometimes you are simply missing a limit clause in your query. This might be the case if you have an implied
77
+ upper bound on the number of results enforced by the application elsewhere. SafeQuery will find cases where this limit isn't expressed in your queries,
78
+ which might be a problem if your enforcement logic is flawed in some way.
79
+
80
+ ### Ignore this problem
81
+
82
+ You can ignore this problem by converting the relation to an array with `to_a` before you operate on it:
83
+
84
+ ```ruby
85
+ book.authors.find_by ...
86
+ ```
87
+
88
+ to this:
89
+
90
+ ```ruby
91
+ book.authors.to_a.find_by ...
92
+ ```
data/lib/safe_query.rb ADDED
@@ -0,0 +1,47 @@
1
+ module ActiveRecord
2
+ class UnsafeQueryError < StandardError
3
+ # skip ourselves in the backtrace so it ends in the user code that generated the issue
4
+ def backtrace
5
+ return @lines if @lines
6
+ @lines = super
7
+ @lines.shift if @lines.present?
8
+ @lines
9
+ end
10
+ end
11
+
12
+ class Relation
13
+ module SafeQuery
14
+ def each
15
+ QueryRegistry.reset
16
+ super
17
+
18
+ query_to_check = QueryRegistry.queries.first.to_s
19
+
20
+ unless query_to_check.blank? || query_to_check.upcase.include?("LIMIT ") || query_to_check.upcase.include?("IN ")
21
+ raise UnsafeQueryError, "Detected a potentially dangerous #each iterator on an unpaginated query. " +
22
+ "Perhaps you need to add pagination, a limit clause, or use the ActiveRecord::Batches methods. \n\n" +
23
+ "To ignore this problem, or if it is a false positive, convert it to an array with ActiveRecord::Relation#to_a before iterating.\n\n" ++
24
+ "Potentially unpaginated query: \n\n #{query_to_check}"
25
+ end
26
+ end
27
+
28
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
29
+ QueryRegistry.queries << payload[:sql]
30
+ end
31
+
32
+ module QueryRegistry
33
+ extend self
34
+
35
+ def queries
36
+ ActiveSupport::IsolatedExecutionState[:active_record_query_registry] ||= []
37
+ end
38
+
39
+ def reset
40
+ queries.clear
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ ActiveRecord::Relation.prepend ActiveRecord::Relation::SafeQuery
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: safe_query
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter Cai
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '8.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '5.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '8.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: rspec
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '3.12'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '3.12'
67
+ - !ruby/object:Gem::Dependency
68
+ name: sqlite3
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: 1.6.1
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 1.6.1
81
+ description: " Helps developers avoid unsafe queries in ActiveRecord. This gem
82
+ will raise an error \n when iterating over a relation that is potentially unpaginated.\n"
83
+ email: hello@petercai.com
84
+ executables: []
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - CHANGELOG.md
89
+ - LICENSE
90
+ - README.md
91
+ - lib/safe_query.rb
92
+ homepage: https://github.com/pcai/safe_query
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 2.6.0
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.3.26
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Safely query stuff in ActiveRecord
115
+ test_files: []