rails-guarddog 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 859dc0da3cfd03afa3ceea5be6d2feba0f3f49786f109fb82c0a5b1c379c0837
4
- data.tar.gz: a0251fa578fb369a59316ff36e62e4409cbfb0b08bdc4c4be1d2d75b764c1411
3
+ metadata.gz: 97cdcd83511d9b89a1786584d4a21dcf403123b3ea3911e7341a85a3bda88446
4
+ data.tar.gz: e6634444d93c30e0ddf147cb51db25ee7f04f6f2ee9a17f09d9c1de7b59d7da0
5
5
  SHA512:
6
- metadata.gz: 3394a5d46d8ed065003722151b6f5d74361fac8e9656a3c39760e02fc25e581c474cb3246aa9bb8337530a50f70d492dfc77127b1770f7fa82bc6d01460073e5
7
- data.tar.gz: f52bea6bb7b20187c2ab4a1751267f5987116f1839657bda880dfb93e18a96a8840e4621fde4b490c4d4d318c9f42fbe453be04c6514aa0a42c26b4293a40840
6
+ metadata.gz: c1e72af9e429215d30599e42276ced36f320f052002274827beb6b4060adfa29f907c42af0a88da4a13eead365ef84e32cbb079994534084112e60151671a09f
7
+ data.tar.gz: 2af6a3569f894d95fc2e944ebe14bce13696bc89e0120f13aa5c3cdd9e305e44ecbaba77a6e212dc1911ae1bc2efba267c58093e141bb5c544e59c39979aa3d3
data/README.md CHANGED
@@ -104,14 +104,16 @@ That's it! Scan your entire Rails app for security vulnerabilities.
104
104
  - **DoS/ReDoS** — Unbounded queries, dangerous regex patterns ⭐ ENHANCED in v0.1.8
105
105
  - **Supply Chain** — Typosquatted gems using Levenshtein distance ⭐ IMPROVED in v0.1.8
106
106
 
107
- > **False-positive protection (v0.1.10)**
108
- > - Patterns use word boundaries — `no_token:` or `reset_password_instructions:` in locale
107
+ > **False-positive protection (v0.1.10 - v0.1.12)**
108
+ > - **Secrets**: Patterns use word boundaries — `no_token:` or `reset_password_instructions:` in locale
109
109
  > files will **not** trigger the secrets checker.
110
- > - Secret values must be space-free (`[^\s'"]{6,}`) — human-readable sentences like
110
+ > - **Secrets**: Secret values must be space-free (`[^\s'"]{6,}`) — human-readable sentences like
111
111
  > `"You can't access this page..."` are never flagged.
112
- > - `config/locales/` files are skipped entirely — i18n translations are not credentials.
113
- > - `spec/` and `test/` trees suppress the secrets checker by default (configurable via
112
+ > - **Secrets**: `config/locales/` files are skipped entirely — i18n translations are not credentials.
113
+ > - **Secrets**: `spec/` and `test/` trees suppress the secrets checker by default (configurable via
114
114
  > `ignored_checks`).
115
+ > - **DoS**: Smart multiline `.limit()` check detection (v0.1.12) — lookahead up to 3 lines handles chained
116
+ > `.limit()` calls on subsequent lines or reassignment to the same variable, e.g. `things = Thing.all; things = things.limit(100)` or `.limit(...)` on the next line.
115
117
 
116
118
 
117
119
  ## 📊 Example Output
@@ -119,7 +121,7 @@ That's it! Scan your entire Rails app for security vulnerabilities.
119
121
  ### Console Report
120
122
  ```
121
123
  ============================================================
122
- Rails GuardDog Security Report v0.1.10
124
+ Rails GuardDog Security Report v0.1.12
123
125
  ============================================================
124
126
 
125
127
  [CRITICAL] (5 findings)
@@ -290,6 +292,6 @@ MIT License - Free to use and modify.
290
292
 
291
293
  ---
292
294
 
293
- **Rails GuardDog v0.1.10 — Production Ready**
295
+ **Rails GuardDog v0.1.12 — Production Ready**
294
296
 
295
297
  *Beyond brakeman. Detect what others miss.* 🐕🔒
@@ -4,20 +4,19 @@ module Rails
4
4
  class DosChecker < BaseChecker
5
5
  # Matches .all only when it is the final call on an ActiveRecord-style
6
6
  # model class (PascalCase constant), e.g.:
7
- # User.all => flagged
7
+ # User.all => flagged
8
8
  # Post.where(...).all => flagged
9
9
  # But NOT:
10
- # current_user.permissions.track_manage.all => NOT flagged (chained on object)
11
- # collection.all? { } => NOT flagged (.all? method)
12
- # track_manage.all => NOT flagged (lowercase receiver)
10
+ # current_user.permissions.track_manage.all => NOT flagged (lowercase receiver)
11
+ # collection.all? { } => NOT flagged (.all? method)
13
12
  UNBOUNDED_QUERY_PATTERN = /
14
13
  (?:
15
- [A-Z][A-Za-z0-9_]* # PascalCase model class e.g. User, TrackItem
16
- (?:\.[a-z_]+\(.*?\))* # optional chained scopes e.g. .where(...).order(...)
14
+ [A-Z][A-Za-z0-9_]* # PascalCase model class e.g. User, TrackItem
15
+ (?:\.[a-z_]+\(.*?\))* # optional chained scopes e.g. .where(...).order(...)
17
16
  )
18
- \.all # the .all call
19
- (?!\?) # not .all? (Enumerable method, not AR query)
20
- \s*(?:[,)\]#\n]|$) # followed by end-of-expression
17
+ \.all # the .all call
18
+ (?!\?) # not .all? (Enumerable method, not AR query)
19
+ \s*(?:[,)\]#\n]|$) # followed by end-of-expression (not .limit etc.)
21
20
  /x.freeze
22
21
 
23
22
  # Patterns known to be dangerous nested regex (ReDoS)
@@ -26,13 +25,18 @@ module Rails
26
25
  /match\?.*\(.+\*\+/
27
26
  ].freeze
28
27
 
28
+ # How many lines ahead to search for a .limit() call.
29
+ # Covers both chained-on-next-line and separate-assignment styles.
30
+ LOOKAHEAD_LINES = 3
31
+
29
32
  def run
30
33
  glob_files('app/**/*.rb').each do |file|
31
- content = File.read(file) rescue next
32
- content.each_line.with_index do |line, idx|
34
+ lines = File.readlines(file) rescue next
35
+
36
+ lines.each_with_index do |line, idx|
33
37
  next if line.strip.start_with?('#')
34
38
 
35
- if line.match?(UNBOUNDED_QUERY_PATTERN)
39
+ if line.match?(UNBOUNDED_QUERY_PATTERN) && !limit_applied?(lines, idx, line)
36
40
  add_finding(
37
41
  severity: :high,
38
42
  message: "Potential DoS: unbounded database query without limit",
@@ -57,6 +61,46 @@ module Rails
57
61
  end
58
62
  findings
59
63
  end
64
+
65
+ private
66
+
67
+ # Returns true if a .limit() call is found either:
68
+ # 1. On the same line as .all (e.g. Thing.all.limit(100))
69
+ # 2. Chained on the very next line (e.g. Thing.all\n .limit(100))
70
+ # 3. Applied to the assigned variable within LOOKAHEAD_LINES lines
71
+ # (e.g. things = Thing.all \n things = things.limit(100))
72
+ def limit_applied?(lines, current_idx, current_line)
73
+ # 1. Same-line limit (Thing.all.limit(100)) — already excluded by
74
+ # UNBOUNDED_QUERY_PATTERN's end-of-expression anchor, but double-check.
75
+ return true if current_line.match?(/\.limit\s*\(/)
76
+
77
+ # Extract the variable being assigned, supporting local vars, instance
78
+ # vars (@foo) and class-instance vars (@@foo).
79
+ assigned_var = current_line.match(/([@]{0,2}\w+)\s*=\s*[A-Z]/)&.[](1)
80
+
81
+ lookahead = lines[(current_idx + 1), LOOKAHEAD_LINES] || []
82
+
83
+ lookahead.each do |next_line|
84
+ stripped = next_line.lstrip
85
+
86
+ # 2. Chained on next line: the continuation starts with .limit
87
+ return true if stripped.start_with?('.limit')
88
+
89
+ # 3. Assigned variable subsequently limited:
90
+ # things = things.limit(100) => local var (word boundary ok)
91
+ # @things = @things.limit(100) => instance var (@ is non-word, use look-behind)
92
+ # things.limit(100)
93
+ if assigned_var
94
+ # Build an anchor that works for both `@var` and plain `var`:
95
+ # require that the var is preceded by start-of-line, whitespace or `=`.
96
+ anchor = '(?:^|[\s=(])'
97
+ escaped = Regexp.escape(assigned_var)
98
+ return true if next_line.match?(/#{anchor}#{escaped}(?:\s*=\s*#{escaped})?\.limit\s*\(/)
99
+ end
100
+ end
101
+
102
+ false
103
+ end
60
104
  end
61
105
  end
62
106
  end
@@ -1,5 +1,5 @@
1
1
  module Rails
2
2
  module Guarddog
3
- VERSION = "0.1.11"
3
+ VERSION = "0.1.12"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-guarddog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Security Team