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 +4 -4
- data/README.md +9 -7
- data/lib/rails/guarddog/checkers/dos_checker.rb +56 -12
- data/lib/rails/guarddog/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97cdcd83511d9b89a1786584d4a21dcf403123b3ea3911e7341a85a3bda88446
|
|
4
|
+
data.tar.gz: e6634444d93c30e0ddf147cb51db25ee7f04f6f2ee9a17f09d9c1de7b59d7da0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
|
7
|
+
# User.all => flagged
|
|
8
8
|
# Post.where(...).all => flagged
|
|
9
9
|
# But NOT:
|
|
10
|
-
# current_user.permissions.track_manage.all
|
|
11
|
-
# collection.all? { }
|
|
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_]*
|
|
16
|
-
(?:\.[a-z_]+\(.*?\))*
|
|
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
|
|
19
|
-
(?!\?)
|
|
20
|
-
\s*(?:[,)\]#\n]|$)
|
|
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
|
-
|
|
32
|
-
|
|
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
|