getdoorman 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby Security Rules (SEC-RUBY-001 through SEC-RUBY-060)
|
|
3
|
+
*
|
|
4
|
+
* Each rule scans Ruby source files for security vulnerabilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const isRuby = (f) => f.endsWith('.rb');
|
|
8
|
+
|
|
9
|
+
const SKIP_PATH = /[/\\](test|tests|__tests__|__mocks__|mocks|fixtures|__fixtures__|spec|__snapshots__|node_modules|vendor|dist|build)[/\\]/i;
|
|
10
|
+
const COMMENT_LINE = /^\s*(#)/;
|
|
11
|
+
|
|
12
|
+
function scanLines(content, regex, file, rule) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
const lines = content.split('\n');
|
|
15
|
+
for (let i = 0; i < lines.length; i++) {
|
|
16
|
+
const line = lines[i];
|
|
17
|
+
if (COMMENT_LINE.test(line)) continue;
|
|
18
|
+
if (regex.test(line)) {
|
|
19
|
+
findings.push({
|
|
20
|
+
ruleId: rule.id,
|
|
21
|
+
category: rule.category,
|
|
22
|
+
severity: rule.severity,
|
|
23
|
+
title: rule.title,
|
|
24
|
+
description: rule.description,
|
|
25
|
+
confidence: rule.confidence,
|
|
26
|
+
file,
|
|
27
|
+
line: i + 1,
|
|
28
|
+
fix: rule.fix || null,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return findings;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checkRuby(rule, files, ...patterns) {
|
|
36
|
+
const findings = [];
|
|
37
|
+
for (const [path, content] of files) {
|
|
38
|
+
if (SKIP_PATH.test(path)) continue;
|
|
39
|
+
if (isRuby(path)) {
|
|
40
|
+
for (const p of patterns) {
|
|
41
|
+
findings.push(...scanLines(content, p, path, rule));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return findings;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rules = [
|
|
49
|
+
// ===========================================================================
|
|
50
|
+
// Rails SQL Injection (001–010)
|
|
51
|
+
// ===========================================================================
|
|
52
|
+
|
|
53
|
+
// SEC-RUBY-001: SQL injection via string interpolation in where clause
|
|
54
|
+
{
|
|
55
|
+
id: 'SEC-RUBY-001',
|
|
56
|
+
category: 'security',
|
|
57
|
+
severity: 'critical',
|
|
58
|
+
confidence: 'likely',
|
|
59
|
+
title: 'SQL Injection via String Interpolation in where()',
|
|
60
|
+
description: 'Using string interpolation inside ActiveRecord where() allows SQL injection.',
|
|
61
|
+
fix: { suggestion: 'Use parameterized where: where("col = ?", value) or where(col: value).' },
|
|
62
|
+
check({ files }) {
|
|
63
|
+
return checkRuby(this, files,
|
|
64
|
+
/\.where\s*\(\s*["'].*#\{/,
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// SEC-RUBY-002: SQL injection via string concatenation in where clause
|
|
70
|
+
{
|
|
71
|
+
id: 'SEC-RUBY-002',
|
|
72
|
+
category: 'security',
|
|
73
|
+
severity: 'critical',
|
|
74
|
+
confidence: 'likely',
|
|
75
|
+
title: 'SQL Injection via String Concatenation in where()',
|
|
76
|
+
description: 'Concatenating user input into ActiveRecord where() enables SQL injection.',
|
|
77
|
+
fix: { suggestion: 'Use parameterized queries instead of string concatenation.' },
|
|
78
|
+
check({ files }) {
|
|
79
|
+
return checkRuby(this, files,
|
|
80
|
+
/\.where\s*\(\s*["'].*["']\s*\+/,
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// SEC-RUBY-003: Raw SQL execution with interpolation
|
|
86
|
+
{
|
|
87
|
+
id: 'SEC-RUBY-003',
|
|
88
|
+
category: 'security',
|
|
89
|
+
severity: 'critical',
|
|
90
|
+
confidence: 'likely',
|
|
91
|
+
title: 'Raw SQL with String Interpolation',
|
|
92
|
+
description: 'Using execute() or select_all() with interpolated strings allows SQL injection.',
|
|
93
|
+
fix: { suggestion: 'Use sanitize_sql_array or parameterized queries.' },
|
|
94
|
+
check({ files }) {
|
|
95
|
+
return checkRuby(this, files,
|
|
96
|
+
/(?:execute|select_all|select_one|select_value)\s*\(\s*["'].*#\{/,
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// SEC-RUBY-004: SQL injection via find_by_sql
|
|
102
|
+
{
|
|
103
|
+
id: 'SEC-RUBY-004',
|
|
104
|
+
category: 'security',
|
|
105
|
+
severity: 'critical',
|
|
106
|
+
confidence: 'likely',
|
|
107
|
+
title: 'SQL Injection via find_by_sql',
|
|
108
|
+
description: 'Using find_by_sql with string interpolation allows SQL injection.',
|
|
109
|
+
fix: { suggestion: 'Use parameterized find_by_sql: find_by_sql(["SELECT ... WHERE id = ?", id]).' },
|
|
110
|
+
check({ files }) {
|
|
111
|
+
return checkRuby(this, files,
|
|
112
|
+
/find_by_sql\s*\(\s*["'].*#\{/,
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// SEC-RUBY-005: SQL injection via order clause
|
|
118
|
+
{
|
|
119
|
+
id: 'SEC-RUBY-005',
|
|
120
|
+
category: 'security',
|
|
121
|
+
severity: 'high',
|
|
122
|
+
confidence: 'likely',
|
|
123
|
+
title: 'SQL Injection via order()',
|
|
124
|
+
description: 'Passing user input directly to order() can allow SQL injection.',
|
|
125
|
+
fix: { suggestion: 'Whitelist allowed columns and directions for ordering.' },
|
|
126
|
+
check({ files }) {
|
|
127
|
+
return checkRuby(this, files,
|
|
128
|
+
/\.order\s*\(\s*["'].*#\{/,
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// SEC-RUBY-006: SQL injection via group clause
|
|
134
|
+
{
|
|
135
|
+
id: 'SEC-RUBY-006',
|
|
136
|
+
category: 'security',
|
|
137
|
+
severity: 'high',
|
|
138
|
+
confidence: 'likely',
|
|
139
|
+
title: 'SQL Injection via group()',
|
|
140
|
+
description: 'Passing user input to group() enables SQL injection.',
|
|
141
|
+
fix: { suggestion: 'Whitelist allowed column names for grouping.' },
|
|
142
|
+
check({ files }) {
|
|
143
|
+
return checkRuby(this, files,
|
|
144
|
+
/\.group\s*\(\s*["'].*#\{/,
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// SEC-RUBY-007: SQL injection via pluck
|
|
150
|
+
{
|
|
151
|
+
id: 'SEC-RUBY-007',
|
|
152
|
+
category: 'security',
|
|
153
|
+
severity: 'high',
|
|
154
|
+
confidence: 'likely',
|
|
155
|
+
title: 'SQL Injection via pluck()',
|
|
156
|
+
description: 'Passing user input to pluck() can lead to SQL injection.',
|
|
157
|
+
fix: { suggestion: 'Whitelist column names before passing to pluck.' },
|
|
158
|
+
check({ files }) {
|
|
159
|
+
return checkRuby(this, files,
|
|
160
|
+
/\.pluck\s*\(\s*["'].*#\{/,
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// SEC-RUBY-008: SQL injection via having clause
|
|
166
|
+
{
|
|
167
|
+
id: 'SEC-RUBY-008',
|
|
168
|
+
category: 'security',
|
|
169
|
+
severity: 'high',
|
|
170
|
+
confidence: 'likely',
|
|
171
|
+
title: 'SQL Injection via having()',
|
|
172
|
+
description: 'Using string interpolation in having() allows SQL injection.',
|
|
173
|
+
fix: { suggestion: 'Use parameterized having: having("count > ?", value).' },
|
|
174
|
+
check({ files }) {
|
|
175
|
+
return checkRuby(this, files,
|
|
176
|
+
/\.having\s*\(\s*["'].*#\{/,
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// SEC-RUBY-009: SQL injection via joins
|
|
182
|
+
{
|
|
183
|
+
id: 'SEC-RUBY-009',
|
|
184
|
+
category: 'security',
|
|
185
|
+
severity: 'high',
|
|
186
|
+
confidence: 'likely',
|
|
187
|
+
title: 'SQL Injection via joins()',
|
|
188
|
+
description: 'Using string interpolation in joins() allows SQL injection.',
|
|
189
|
+
fix: { suggestion: 'Use symbol-based joins or sanitize input.' },
|
|
190
|
+
check({ files }) {
|
|
191
|
+
return checkRuby(this, files,
|
|
192
|
+
/\.joins\s*\(\s*["'].*#\{/,
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// SEC-RUBY-010: SQL injection via calculate
|
|
198
|
+
{
|
|
199
|
+
id: 'SEC-RUBY-010',
|
|
200
|
+
category: 'security',
|
|
201
|
+
severity: 'high',
|
|
202
|
+
confidence: 'likely',
|
|
203
|
+
title: 'SQL Injection via calculate()',
|
|
204
|
+
description: 'Passing user-controlled strings to calculate() enables SQL injection.',
|
|
205
|
+
fix: { suggestion: 'Whitelist allowed aggregate operations and column names.' },
|
|
206
|
+
check({ files }) {
|
|
207
|
+
return checkRuby(this, files,
|
|
208
|
+
/\.calculate\s*\(\s*["'].*#\{/,
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
// ===========================================================================
|
|
214
|
+
// XSS (011–015)
|
|
215
|
+
// ===========================================================================
|
|
216
|
+
|
|
217
|
+
// SEC-RUBY-011: XSS via raw() helper
|
|
218
|
+
{
|
|
219
|
+
id: 'SEC-RUBY-011',
|
|
220
|
+
category: 'security',
|
|
221
|
+
severity: 'high',
|
|
222
|
+
confidence: 'likely',
|
|
223
|
+
title: 'XSS via raw() Helper',
|
|
224
|
+
description: 'Using raw() to render user input bypasses HTML escaping and enables XSS.',
|
|
225
|
+
fix: { suggestion: 'Use sanitize() or html_escape() instead of raw().' },
|
|
226
|
+
check({ files }) {
|
|
227
|
+
return checkRuby(this, files,
|
|
228
|
+
/\braw\s*\(/,
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
// SEC-RUBY-012: XSS via html_safe
|
|
234
|
+
{
|
|
235
|
+
id: 'SEC-RUBY-012',
|
|
236
|
+
category: 'security',
|
|
237
|
+
severity: 'high',
|
|
238
|
+
confidence: 'likely',
|
|
239
|
+
title: 'XSS via html_safe',
|
|
240
|
+
description: 'Calling html_safe on user-controlled strings disables HTML escaping.',
|
|
241
|
+
fix: { suggestion: 'Avoid html_safe on user input; use sanitize() instead.' },
|
|
242
|
+
check({ files }) {
|
|
243
|
+
return checkRuby(this, files,
|
|
244
|
+
/\.html_safe/,
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
// SEC-RUBY-013: XSS via content_tag with unsafe content
|
|
250
|
+
{
|
|
251
|
+
id: 'SEC-RUBY-013',
|
|
252
|
+
category: 'security',
|
|
253
|
+
severity: 'medium',
|
|
254
|
+
confidence: 'suggestion',
|
|
255
|
+
title: 'XSS via content_tag with Interpolation',
|
|
256
|
+
description: 'Using content_tag with interpolated user input may lead to XSS.',
|
|
257
|
+
fix: { suggestion: 'Ensure all content passed to content_tag is sanitized.' },
|
|
258
|
+
check({ files }) {
|
|
259
|
+
return checkRuby(this, files,
|
|
260
|
+
/content_tag\s*\(.*#\{/,
|
|
261
|
+
);
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// SEC-RUBY-014: XSS via link_to with user-controlled href
|
|
266
|
+
{
|
|
267
|
+
id: 'SEC-RUBY-014',
|
|
268
|
+
category: 'security',
|
|
269
|
+
severity: 'medium',
|
|
270
|
+
confidence: 'suggestion',
|
|
271
|
+
title: 'XSS via link_to with User-Controlled URL',
|
|
272
|
+
description: 'Using link_to with user-controlled URLs can allow javascript: protocol XSS.',
|
|
273
|
+
fix: { suggestion: 'Validate URLs against an allowlist of protocols (http, https).' },
|
|
274
|
+
check({ files }) {
|
|
275
|
+
return checkRuby(this, files,
|
|
276
|
+
/link_to\s*.*params\[/,
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
// SEC-RUBY-015: XSS via render inline
|
|
282
|
+
{
|
|
283
|
+
id: 'SEC-RUBY-015',
|
|
284
|
+
category: 'security',
|
|
285
|
+
severity: 'high',
|
|
286
|
+
confidence: 'likely',
|
|
287
|
+
title: 'XSS via render inline',
|
|
288
|
+
description: 'Using render inline with user input allows arbitrary code execution and XSS.',
|
|
289
|
+
fix: { suggestion: 'Avoid render inline; use templates with proper escaping.' },
|
|
290
|
+
check({ files }) {
|
|
291
|
+
return checkRuby(this, files,
|
|
292
|
+
/render\s+inline\s*:/,
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
// ===========================================================================
|
|
298
|
+
// Mass Assignment (016–020)
|
|
299
|
+
// ===========================================================================
|
|
300
|
+
|
|
301
|
+
// SEC-RUBY-016: Mass assignment via permit!
|
|
302
|
+
{
|
|
303
|
+
id: 'SEC-RUBY-016',
|
|
304
|
+
category: 'security',
|
|
305
|
+
severity: 'high',
|
|
306
|
+
confidence: 'likely',
|
|
307
|
+
title: 'Mass Assignment via permit!',
|
|
308
|
+
description: 'Using permit! allows all parameters, bypassing strong parameters protection.',
|
|
309
|
+
fix: { suggestion: 'Explicitly list allowed parameters with permit(:field1, :field2).' },
|
|
310
|
+
check({ files }) {
|
|
311
|
+
return checkRuby(this, files,
|
|
312
|
+
/\.permit!/,
|
|
313
|
+
);
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// SEC-RUBY-017: Mass assignment via attr_accessible bypass
|
|
318
|
+
{
|
|
319
|
+
id: 'SEC-RUBY-017',
|
|
320
|
+
category: 'security',
|
|
321
|
+
severity: 'high',
|
|
322
|
+
confidence: 'likely',
|
|
323
|
+
title: 'Unprotected Mass Assignment with attr_accessible',
|
|
324
|
+
description: 'Using without_protection: true bypasses attr_accessible whitelisting.',
|
|
325
|
+
fix: { suggestion: 'Remove without_protection option and properly whitelist attributes.' },
|
|
326
|
+
check({ files }) {
|
|
327
|
+
return checkRuby(this, files,
|
|
328
|
+
/without_protection\s*:\s*true/,
|
|
329
|
+
);
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
// SEC-RUBY-018: Mass assignment via update_attributes with params
|
|
334
|
+
{
|
|
335
|
+
id: 'SEC-RUBY-018',
|
|
336
|
+
category: 'security',
|
|
337
|
+
severity: 'high',
|
|
338
|
+
confidence: 'likely',
|
|
339
|
+
title: 'Mass Assignment via update_attributes with params',
|
|
340
|
+
description: 'Passing raw params to update_attributes allows mass assignment attacks.',
|
|
341
|
+
fix: { suggestion: 'Use strong parameters: update_attributes(user_params) with permit.' },
|
|
342
|
+
check({ files }) {
|
|
343
|
+
return checkRuby(this, files,
|
|
344
|
+
/update_attributes?\s*\(\s*params/,
|
|
345
|
+
);
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// SEC-RUBY-019: Mass assignment via new/create with raw params
|
|
350
|
+
{
|
|
351
|
+
id: 'SEC-RUBY-019',
|
|
352
|
+
category: 'security',
|
|
353
|
+
severity: 'high',
|
|
354
|
+
confidence: 'likely',
|
|
355
|
+
title: 'Mass Assignment via new/create with Raw Params',
|
|
356
|
+
description: 'Passing raw params hash to new() or create() enables mass assignment.',
|
|
357
|
+
fix: { suggestion: 'Use strong parameters to whitelist allowed fields.' },
|
|
358
|
+
check({ files }) {
|
|
359
|
+
return checkRuby(this, files,
|
|
360
|
+
/\.(?:new|create|create!)\s*\(\s*params\b/,
|
|
361
|
+
);
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
// SEC-RUBY-020: Mass assignment via assign_attributes
|
|
366
|
+
{
|
|
367
|
+
id: 'SEC-RUBY-020',
|
|
368
|
+
category: 'security',
|
|
369
|
+
severity: 'high',
|
|
370
|
+
confidence: 'likely',
|
|
371
|
+
title: 'Mass Assignment via assign_attributes with Raw Params',
|
|
372
|
+
description: 'Passing raw params to assign_attributes allows mass assignment.',
|
|
373
|
+
fix: { suggestion: 'Filter params through strong parameters before assign_attributes.' },
|
|
374
|
+
check({ files }) {
|
|
375
|
+
return checkRuby(this, files,
|
|
376
|
+
/assign_attributes\s*\(\s*params/,
|
|
377
|
+
);
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
// ===========================================================================
|
|
382
|
+
// CSRF (021–025)
|
|
383
|
+
// ===========================================================================
|
|
384
|
+
|
|
385
|
+
// SEC-RUBY-021: CSRF protection disabled
|
|
386
|
+
{
|
|
387
|
+
id: 'SEC-RUBY-021',
|
|
388
|
+
category: 'security',
|
|
389
|
+
severity: 'high',
|
|
390
|
+
confidence: 'likely',
|
|
391
|
+
title: 'CSRF Protection Disabled',
|
|
392
|
+
description: 'skip_before_action :verify_authenticity_token disables CSRF protection.',
|
|
393
|
+
fix: { suggestion: 'Remove the skip_before_action or scope it to API-only actions with proper auth.' },
|
|
394
|
+
check({ files }) {
|
|
395
|
+
return checkRuby(this, files,
|
|
396
|
+
/skip_before_action\s+:verify_authenticity_token/,
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
// SEC-RUBY-022: CSRF protect_from_forgery with null_session
|
|
402
|
+
{
|
|
403
|
+
id: 'SEC-RUBY-022',
|
|
404
|
+
category: 'security',
|
|
405
|
+
severity: 'medium',
|
|
406
|
+
confidence: 'suggestion',
|
|
407
|
+
title: 'CSRF Protection with null_session',
|
|
408
|
+
description: 'protect_from_forgery with: :null_session may weaken CSRF protection.',
|
|
409
|
+
fix: { suggestion: 'Use protect_from_forgery with: :exception for web apps.' },
|
|
410
|
+
check({ files }) {
|
|
411
|
+
return checkRuby(this, files,
|
|
412
|
+
/protect_from_forgery\s+with:\s*:null_session/,
|
|
413
|
+
);
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
// SEC-RUBY-023: Missing CSRF protection
|
|
418
|
+
{
|
|
419
|
+
id: 'SEC-RUBY-023',
|
|
420
|
+
category: 'security',
|
|
421
|
+
severity: 'high',
|
|
422
|
+
confidence: 'suggestion',
|
|
423
|
+
title: 'Missing protect_from_forgery in ApplicationController',
|
|
424
|
+
description: 'ApplicationController without protect_from_forgery is vulnerable to CSRF.',
|
|
425
|
+
fix: { suggestion: 'Add protect_from_forgery with: :exception to ApplicationController.' },
|
|
426
|
+
check({ files }) {
|
|
427
|
+
const findings = [];
|
|
428
|
+
for (const [path, content] of files) {
|
|
429
|
+
if (SKIP_PATH.test(path)) continue;
|
|
430
|
+
if (isRuby(path)) {
|
|
431
|
+
if (/class\s+ApplicationController/.test(content) && /skip_forgery_protection/.test(content)) {
|
|
432
|
+
findings.push({ ruleId: this.id, file: path, line: 1, title: this.title, severity: this.severity });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return findings;
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
// SEC-RUBY-024: CSRF token in GET request
|
|
441
|
+
{
|
|
442
|
+
id: 'SEC-RUBY-024',
|
|
443
|
+
category: 'security',
|
|
444
|
+
severity: 'medium',
|
|
445
|
+
confidence: 'suggestion',
|
|
446
|
+
title: 'State-Changing Action via GET Route',
|
|
447
|
+
description: 'Using GET for state-changing actions exposes CSRF tokens in URLs and logs.',
|
|
448
|
+
fix: { suggestion: 'Use POST/PUT/PATCH/DELETE for state-changing operations.' },
|
|
449
|
+
check({ files }) {
|
|
450
|
+
return checkRuby(this, files,
|
|
451
|
+
/get\s+['"].*(?:delete|destroy|update|create|remove)/,
|
|
452
|
+
);
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
// SEC-RUBY-025: CSRF protection disabled globally
|
|
457
|
+
{
|
|
458
|
+
id: 'SEC-RUBY-025',
|
|
459
|
+
category: 'security',
|
|
460
|
+
severity: 'critical',
|
|
461
|
+
confidence: 'likely',
|
|
462
|
+
title: 'CSRF Protection Disabled via Configuration',
|
|
463
|
+
description: 'Setting allow_forgery_protection to false disables CSRF globally.',
|
|
464
|
+
fix: { suggestion: 'Set config.action_controller.allow_forgery_protection = true.' },
|
|
465
|
+
check({ files }) {
|
|
466
|
+
return checkRuby(this, files,
|
|
467
|
+
/allow_forgery_protection\s*=\s*false/,
|
|
468
|
+
);
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
// ===========================================================================
|
|
473
|
+
// Command Injection (026–030)
|
|
474
|
+
// ===========================================================================
|
|
475
|
+
|
|
476
|
+
// SEC-RUBY-026: Command injection via system()
|
|
477
|
+
{
|
|
478
|
+
id: 'SEC-RUBY-026',
|
|
479
|
+
category: 'security',
|
|
480
|
+
severity: 'critical',
|
|
481
|
+
confidence: 'likely',
|
|
482
|
+
title: 'Command Injection via system()',
|
|
483
|
+
description: 'Passing user input to system() allows command injection.',
|
|
484
|
+
fix: { suggestion: 'Use the array form: system("cmd", arg1, arg2) to avoid shell interpretation.' },
|
|
485
|
+
check({ files }) {
|
|
486
|
+
return checkRuby(this, files,
|
|
487
|
+
/\bsystem\s*\(\s*["'].*#\{/,
|
|
488
|
+
);
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
// SEC-RUBY-027: Command injection via backticks
|
|
493
|
+
{
|
|
494
|
+
id: 'SEC-RUBY-027',
|
|
495
|
+
category: 'security',
|
|
496
|
+
severity: 'critical',
|
|
497
|
+
confidence: 'likely',
|
|
498
|
+
title: 'Command Injection via Backticks',
|
|
499
|
+
description: 'Using backticks with interpolation allows command injection.',
|
|
500
|
+
fix: { suggestion: 'Use Open3.capture3 or system() array form instead of backticks.' },
|
|
501
|
+
check({ files }) {
|
|
502
|
+
return checkRuby(this, files,
|
|
503
|
+
/`[^`]*#\{/,
|
|
504
|
+
);
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
// SEC-RUBY-028: Command injection via exec()
|
|
509
|
+
{
|
|
510
|
+
id: 'SEC-RUBY-028',
|
|
511
|
+
category: 'security',
|
|
512
|
+
severity: 'critical',
|
|
513
|
+
confidence: 'likely',
|
|
514
|
+
title: 'Command Injection via exec()',
|
|
515
|
+
description: 'Passing user input to exec() allows command injection.',
|
|
516
|
+
fix: { suggestion: 'Use exec with array form: exec("cmd", arg1) to avoid shell interpretation.' },
|
|
517
|
+
check({ files }) {
|
|
518
|
+
return checkRuby(this, files,
|
|
519
|
+
/\bexec\s*\(\s*["'].*#\{/,
|
|
520
|
+
);
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
// SEC-RUBY-029: Command injection via IO.popen
|
|
525
|
+
{
|
|
526
|
+
id: 'SEC-RUBY-029',
|
|
527
|
+
category: 'security',
|
|
528
|
+
severity: 'critical',
|
|
529
|
+
confidence: 'likely',
|
|
530
|
+
title: 'Command Injection via IO.popen',
|
|
531
|
+
description: 'Using IO.popen with string interpolation allows command injection.',
|
|
532
|
+
fix: { suggestion: 'Use IO.popen with array form: IO.popen(["cmd", arg]).' },
|
|
533
|
+
check({ files }) {
|
|
534
|
+
return checkRuby(this, files,
|
|
535
|
+
/IO\.popen\s*\(\s*["'].*#\{/,
|
|
536
|
+
);
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
// SEC-RUBY-030: Command injection via Open3
|
|
541
|
+
{
|
|
542
|
+
id: 'SEC-RUBY-030',
|
|
543
|
+
category: 'security',
|
|
544
|
+
severity: 'critical',
|
|
545
|
+
confidence: 'likely',
|
|
546
|
+
title: 'Command Injection via Open3 with Interpolation',
|
|
547
|
+
description: 'Using Open3 methods with string interpolation allows command injection.',
|
|
548
|
+
fix: { suggestion: 'Use Open3 with array arguments to avoid shell interpretation.' },
|
|
549
|
+
check({ files }) {
|
|
550
|
+
return checkRuby(this, files,
|
|
551
|
+
/Open3\.(?:capture3|popen3|popen2)\s*\(\s*["'].*#\{/,
|
|
552
|
+
);
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
// ===========================================================================
|
|
557
|
+
// YAML/Marshal Deserialization (031–035)
|
|
558
|
+
// ===========================================================================
|
|
559
|
+
|
|
560
|
+
// SEC-RUBY-031: Unsafe YAML.load
|
|
561
|
+
{
|
|
562
|
+
id: 'SEC-RUBY-031',
|
|
563
|
+
category: 'security',
|
|
564
|
+
severity: 'critical',
|
|
565
|
+
confidence: 'likely',
|
|
566
|
+
title: 'Unsafe YAML.load',
|
|
567
|
+
description: 'YAML.load can deserialize arbitrary Ruby objects, leading to RCE.',
|
|
568
|
+
fix: { suggestion: 'Use YAML.safe_load instead of YAML.load.' },
|
|
569
|
+
check({ files }) {
|
|
570
|
+
return checkRuby(this, files,
|
|
571
|
+
/YAML\.load\s*\(/,
|
|
572
|
+
);
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
// SEC-RUBY-032: Marshal.load with untrusted data
|
|
577
|
+
{
|
|
578
|
+
id: 'SEC-RUBY-032',
|
|
579
|
+
category: 'security',
|
|
580
|
+
severity: 'critical',
|
|
581
|
+
confidence: 'likely',
|
|
582
|
+
title: 'Unsafe Marshal.load',
|
|
583
|
+
description: 'Marshal.load can execute arbitrary code when deserializing untrusted data.',
|
|
584
|
+
fix: { suggestion: 'Use JSON or YAML.safe_load instead of Marshal for untrusted data.' },
|
|
585
|
+
check({ files }) {
|
|
586
|
+
return checkRuby(this, files,
|
|
587
|
+
/Marshal\.load\s*\(/,
|
|
588
|
+
);
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
// SEC-RUBY-033: Marshal.restore with untrusted data
|
|
593
|
+
{
|
|
594
|
+
id: 'SEC-RUBY-033',
|
|
595
|
+
category: 'security',
|
|
596
|
+
severity: 'critical',
|
|
597
|
+
confidence: 'likely',
|
|
598
|
+
title: 'Unsafe Marshal.restore',
|
|
599
|
+
description: 'Marshal.restore is an alias for Marshal.load and is equally dangerous.',
|
|
600
|
+
fix: { suggestion: 'Use JSON or YAML.safe_load instead of Marshal.restore.' },
|
|
601
|
+
check({ files }) {
|
|
602
|
+
return checkRuby(this, files,
|
|
603
|
+
/Marshal\.restore\s*\(/,
|
|
604
|
+
);
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
// SEC-RUBY-034: YAML.load with permitted_classes bypass
|
|
609
|
+
{
|
|
610
|
+
id: 'SEC-RUBY-034',
|
|
611
|
+
category: 'security',
|
|
612
|
+
severity: 'high',
|
|
613
|
+
confidence: 'suggestion',
|
|
614
|
+
title: 'YAML.safe_load with Overly Broad permitted_classes',
|
|
615
|
+
description: 'Allowing too many classes in YAML.safe_load can still be exploitable.',
|
|
616
|
+
fix: { suggestion: 'Minimize permitted_classes to only those strictly needed.' },
|
|
617
|
+
check({ files }) {
|
|
618
|
+
return checkRuby(this, files,
|
|
619
|
+
/YAML\.safe_load\s*\(.*permitted_classes\s*:\s*\[.*Symbol/,
|
|
620
|
+
);
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
// SEC-RUBY-035: ERB.new with user input
|
|
625
|
+
{
|
|
626
|
+
id: 'SEC-RUBY-035',
|
|
627
|
+
category: 'security',
|
|
628
|
+
severity: 'critical',
|
|
629
|
+
confidence: 'likely',
|
|
630
|
+
title: 'Server-Side Template Injection via ERB.new',
|
|
631
|
+
description: 'Passing user input to ERB.new allows arbitrary code execution.',
|
|
632
|
+
fix: { suggestion: 'Never pass user input to ERB.new; use pre-defined templates.' },
|
|
633
|
+
check({ files }) {
|
|
634
|
+
return checkRuby(this, files,
|
|
635
|
+
/ERB\.new\s*\(.*params/,
|
|
636
|
+
);
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
// ===========================================================================
|
|
641
|
+
// File Access (036–040)
|
|
642
|
+
// ===========================================================================
|
|
643
|
+
|
|
644
|
+
// SEC-RUBY-036: Path traversal via File.read
|
|
645
|
+
{
|
|
646
|
+
id: 'SEC-RUBY-036',
|
|
647
|
+
category: 'security',
|
|
648
|
+
severity: 'high',
|
|
649
|
+
confidence: 'likely',
|
|
650
|
+
title: 'Path Traversal via File.read with User Input',
|
|
651
|
+
description: 'Passing user input to File.read allows reading arbitrary files.',
|
|
652
|
+
fix: { suggestion: 'Validate and sanitize file paths; use File.expand_path and check prefix.' },
|
|
653
|
+
check({ files }) {
|
|
654
|
+
return checkRuby(this, files,
|
|
655
|
+
/File\.read\s*\(.*params/,
|
|
656
|
+
);
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
// SEC-RUBY-037: Path traversal via File.open
|
|
661
|
+
{
|
|
662
|
+
id: 'SEC-RUBY-037',
|
|
663
|
+
category: 'security',
|
|
664
|
+
severity: 'high',
|
|
665
|
+
confidence: 'likely',
|
|
666
|
+
title: 'Path Traversal via File.open with User Input',
|
|
667
|
+
description: 'Passing user input to File.open allows reading or writing arbitrary files.',
|
|
668
|
+
fix: { suggestion: 'Validate file paths against a base directory allowlist.' },
|
|
669
|
+
check({ files }) {
|
|
670
|
+
return checkRuby(this, files,
|
|
671
|
+
/File\.open\s*\(.*params/,
|
|
672
|
+
);
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
// SEC-RUBY-038: send_file with user input
|
|
677
|
+
{
|
|
678
|
+
id: 'SEC-RUBY-038',
|
|
679
|
+
category: 'security',
|
|
680
|
+
severity: 'high',
|
|
681
|
+
confidence: 'likely',
|
|
682
|
+
title: 'Path Traversal via send_file',
|
|
683
|
+
description: 'Using send_file with user-controlled path allows arbitrary file disclosure.',
|
|
684
|
+
fix: { suggestion: 'Validate the file path and ensure it stays within the allowed directory.' },
|
|
685
|
+
check({ files }) {
|
|
686
|
+
return checkRuby(this, files,
|
|
687
|
+
/send_file\s*\(.*params/,
|
|
688
|
+
);
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
// SEC-RUBY-039: Tempfile with predictable name
|
|
693
|
+
{
|
|
694
|
+
id: 'SEC-RUBY-039',
|
|
695
|
+
category: 'security',
|
|
696
|
+
severity: 'medium',
|
|
697
|
+
confidence: 'suggestion',
|
|
698
|
+
title: 'Tempfile with Predictable Prefix',
|
|
699
|
+
description: 'Creating temporary files with predictable names may lead to symlink attacks.',
|
|
700
|
+
fix: { suggestion: 'Use SecureRandom for temp file names or let Tempfile handle naming.' },
|
|
701
|
+
check({ files }) {
|
|
702
|
+
return checkRuby(this, files,
|
|
703
|
+
/File\.(?:write|open)\s*\(\s*["']\/tmp\//,
|
|
704
|
+
);
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
// SEC-RUBY-040: File.delete with user input
|
|
709
|
+
{
|
|
710
|
+
id: 'SEC-RUBY-040',
|
|
711
|
+
category: 'security',
|
|
712
|
+
severity: 'high',
|
|
713
|
+
confidence: 'likely',
|
|
714
|
+
title: 'Arbitrary File Deletion',
|
|
715
|
+
description: 'Passing user input to File.delete or FileUtils.rm allows arbitrary file deletion.',
|
|
716
|
+
fix: { suggestion: 'Validate file paths and restrict to allowed directories.' },
|
|
717
|
+
check({ files }) {
|
|
718
|
+
return checkRuby(this, files,
|
|
719
|
+
/(?:File\.delete|FileUtils\.rm)\s*\(.*params/,
|
|
720
|
+
);
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
// ===========================================================================
|
|
725
|
+
// Metaprogramming (041–045)
|
|
726
|
+
// ===========================================================================
|
|
727
|
+
|
|
728
|
+
// SEC-RUBY-041: eval with user input
|
|
729
|
+
{
|
|
730
|
+
id: 'SEC-RUBY-041',
|
|
731
|
+
category: 'security',
|
|
732
|
+
severity: 'critical',
|
|
733
|
+
confidence: 'likely',
|
|
734
|
+
title: 'Code Injection via eval()',
|
|
735
|
+
description: 'Using eval with user-controlled input allows arbitrary code execution.',
|
|
736
|
+
fix: { suggestion: 'Avoid eval entirely; use safe alternatives like case/when dispatching.' },
|
|
737
|
+
check({ files }) {
|
|
738
|
+
return checkRuby(this, files,
|
|
739
|
+
/\beval\s*\(\s*["'].*#\{/,
|
|
740
|
+
);
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
|
|
744
|
+
// SEC-RUBY-042: send with user input
|
|
745
|
+
{
|
|
746
|
+
id: 'SEC-RUBY-042',
|
|
747
|
+
category: 'security',
|
|
748
|
+
severity: 'high',
|
|
749
|
+
confidence: 'likely',
|
|
750
|
+
title: 'Unsafe Dynamic Dispatch via send()',
|
|
751
|
+
description: 'Using send() with user-controlled method names allows calling any method.',
|
|
752
|
+
fix: { suggestion: 'Whitelist allowed method names before using send().' },
|
|
753
|
+
check({ files }) {
|
|
754
|
+
return checkRuby(this, files,
|
|
755
|
+
/\.send\s*\(\s*params/,
|
|
756
|
+
);
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
// SEC-RUBY-043: constantize with user input
|
|
761
|
+
{
|
|
762
|
+
id: 'SEC-RUBY-043',
|
|
763
|
+
category: 'security',
|
|
764
|
+
severity: 'critical',
|
|
765
|
+
confidence: 'likely',
|
|
766
|
+
title: 'Unsafe constantize with User Input',
|
|
767
|
+
description: 'Using constantize on user input allows instantiation of arbitrary classes.',
|
|
768
|
+
fix: { suggestion: 'Whitelist allowed class names before calling constantize.' },
|
|
769
|
+
check({ files }) {
|
|
770
|
+
return checkRuby(this, files,
|
|
771
|
+
/params\[.*\]\.constantize/,
|
|
772
|
+
);
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
|
|
776
|
+
// SEC-RUBY-044: class_eval with user input
|
|
777
|
+
{
|
|
778
|
+
id: 'SEC-RUBY-044',
|
|
779
|
+
category: 'security',
|
|
780
|
+
severity: 'critical',
|
|
781
|
+
confidence: 'likely',
|
|
782
|
+
title: 'Code Injection via class_eval',
|
|
783
|
+
description: 'Using class_eval with interpolated strings allows arbitrary code execution.',
|
|
784
|
+
fix: { suggestion: 'Use define_method or block form of class_eval instead of string eval.' },
|
|
785
|
+
check({ files }) {
|
|
786
|
+
return checkRuby(this, files,
|
|
787
|
+
/class_eval\s*\(\s*["'].*#\{/,
|
|
788
|
+
);
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
|
|
792
|
+
// SEC-RUBY-045: instance_variable_set with user input
|
|
793
|
+
{
|
|
794
|
+
id: 'SEC-RUBY-045',
|
|
795
|
+
category: 'security',
|
|
796
|
+
severity: 'high',
|
|
797
|
+
confidence: 'likely',
|
|
798
|
+
title: 'Unsafe instance_variable_set with User Input',
|
|
799
|
+
description: 'Using instance_variable_set with user-controlled names allows manipulation of internal state.',
|
|
800
|
+
fix: { suggestion: 'Whitelist allowed variable names before using instance_variable_set.' },
|
|
801
|
+
check({ files }) {
|
|
802
|
+
return checkRuby(this, files,
|
|
803
|
+
/instance_variable_set\s*\(.*params/,
|
|
804
|
+
);
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
|
|
808
|
+
// ===========================================================================
|
|
809
|
+
// Crypto (046–050)
|
|
810
|
+
// ===========================================================================
|
|
811
|
+
|
|
812
|
+
// SEC-RUBY-046: Use of MD5
|
|
813
|
+
{
|
|
814
|
+
id: 'SEC-RUBY-046',
|
|
815
|
+
category: 'security',
|
|
816
|
+
severity: 'high',
|
|
817
|
+
confidence: 'likely',
|
|
818
|
+
title: 'Weak Hash: MD5',
|
|
819
|
+
description: 'MD5 is cryptographically broken and should not be used for security purposes.',
|
|
820
|
+
fix: { suggestion: 'Use SHA-256 or SHA-3 via OpenSSL::Digest::SHA256.' },
|
|
821
|
+
check({ files }) {
|
|
822
|
+
return checkRuby(this, files,
|
|
823
|
+
/Digest::MD5/,
|
|
824
|
+
);
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
|
|
828
|
+
// SEC-RUBY-047: Use of SHA1
|
|
829
|
+
{
|
|
830
|
+
id: 'SEC-RUBY-047',
|
|
831
|
+
category: 'security',
|
|
832
|
+
severity: 'medium',
|
|
833
|
+
confidence: 'likely',
|
|
834
|
+
title: 'Weak Hash: SHA1',
|
|
835
|
+
description: 'SHA1 is considered weak for cryptographic purposes.',
|
|
836
|
+
fix: { suggestion: 'Use SHA-256 or SHA-3 instead of SHA1.' },
|
|
837
|
+
check({ files }) {
|
|
838
|
+
return checkRuby(this, files,
|
|
839
|
+
/Digest::SHA1/,
|
|
840
|
+
);
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
|
|
844
|
+
// SEC-RUBY-048: Hardcoded secret key
|
|
845
|
+
{
|
|
846
|
+
id: 'SEC-RUBY-048',
|
|
847
|
+
category: 'security',
|
|
848
|
+
severity: 'critical',
|
|
849
|
+
confidence: 'likely',
|
|
850
|
+
title: 'Hardcoded Secret Key',
|
|
851
|
+
description: 'Hardcoded secret_key_base exposes application to session forgery.',
|
|
852
|
+
fix: { suggestion: 'Use environment variables or credentials file for secret_key_base.' },
|
|
853
|
+
check({ files }) {
|
|
854
|
+
return checkRuby(this, files,
|
|
855
|
+
/secret_key_base\s*=\s*["'][a-f0-9]{30,}/,
|
|
856
|
+
);
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
|
|
860
|
+
// SEC-RUBY-049: Insecure random number generation
|
|
861
|
+
{
|
|
862
|
+
id: 'SEC-RUBY-049',
|
|
863
|
+
category: 'security',
|
|
864
|
+
severity: 'medium',
|
|
865
|
+
confidence: 'likely',
|
|
866
|
+
title: 'Insecure Random Number Generation',
|
|
867
|
+
description: 'Using rand() instead of SecureRandom for security-sensitive values.',
|
|
868
|
+
fix: { suggestion: 'Use SecureRandom.hex or SecureRandom.uuid for tokens and secrets.' },
|
|
869
|
+
check({ files }) {
|
|
870
|
+
return checkRuby(this, files,
|
|
871
|
+
/(?:token|secret|key|password|nonce)\s*=\s*rand\b/,
|
|
872
|
+
);
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
// SEC-RUBY-050: Weak cipher mode (ECB)
|
|
877
|
+
{
|
|
878
|
+
id: 'SEC-RUBY-050',
|
|
879
|
+
category: 'security',
|
|
880
|
+
severity: 'high',
|
|
881
|
+
confidence: 'likely',
|
|
882
|
+
title: 'Weak Cipher Mode: ECB',
|
|
883
|
+
description: 'ECB mode does not provide semantic security and leaks data patterns.',
|
|
884
|
+
fix: { suggestion: 'Use AES-256-GCM or AES-256-CBC with HMAC instead of ECB.' },
|
|
885
|
+
check({ files }) {
|
|
886
|
+
return checkRuby(this, files,
|
|
887
|
+
/OpenSSL::Cipher.*ECB/,
|
|
888
|
+
);
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
|
|
892
|
+
// ===========================================================================
|
|
893
|
+
// Session / Auth (051–055)
|
|
894
|
+
// ===========================================================================
|
|
895
|
+
|
|
896
|
+
// SEC-RUBY-051: Session stored in cookie without encryption
|
|
897
|
+
{
|
|
898
|
+
id: 'SEC-RUBY-051',
|
|
899
|
+
category: 'security',
|
|
900
|
+
severity: 'high',
|
|
901
|
+
confidence: 'likely',
|
|
902
|
+
title: 'Unencrypted Cookie Session Store',
|
|
903
|
+
description: 'Using CookieStore without encryption exposes session data to clients.',
|
|
904
|
+
fix: { suggestion: 'Use encrypted cookie store or database-backed sessions.' },
|
|
905
|
+
check({ files }) {
|
|
906
|
+
return checkRuby(this, files,
|
|
907
|
+
/session_store\s*:cookie_store/,
|
|
908
|
+
);
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
// SEC-RUBY-052: Devise with insecure configuration
|
|
913
|
+
{
|
|
914
|
+
id: 'SEC-RUBY-052',
|
|
915
|
+
category: 'security',
|
|
916
|
+
severity: 'high',
|
|
917
|
+
confidence: 'likely',
|
|
918
|
+
title: 'Devise Insecure stretches Configuration',
|
|
919
|
+
description: 'Setting Devise stretches too low weakens password hashing.',
|
|
920
|
+
fix: { suggestion: 'Set config.stretches to at least 12 in production.' },
|
|
921
|
+
check({ files }) {
|
|
922
|
+
return checkRuby(this, files,
|
|
923
|
+
/config\.stretches\s*=\s*[0-9]\b/,
|
|
924
|
+
);
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
|
|
928
|
+
// SEC-RUBY-053: Authentication bypass via skip_before_action
|
|
929
|
+
{
|
|
930
|
+
id: 'SEC-RUBY-053',
|
|
931
|
+
category: 'security',
|
|
932
|
+
severity: 'high',
|
|
933
|
+
confidence: 'suggestion',
|
|
934
|
+
title: 'Authentication Bypass via skip_before_action',
|
|
935
|
+
description: 'Skipping authenticate_user! may expose actions to unauthenticated users.',
|
|
936
|
+
fix: { suggestion: 'Carefully scope skip_before_action and verify authorization separately.' },
|
|
937
|
+
check({ files }) {
|
|
938
|
+
return checkRuby(this, files,
|
|
939
|
+
/skip_before_action\s+:authenticate/,
|
|
940
|
+
);
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
|
|
944
|
+
// SEC-RUBY-054: Insecure password comparison
|
|
945
|
+
{
|
|
946
|
+
id: 'SEC-RUBY-054',
|
|
947
|
+
category: 'security',
|
|
948
|
+
severity: 'high',
|
|
949
|
+
confidence: 'likely',
|
|
950
|
+
title: 'Insecure Password Comparison',
|
|
951
|
+
description: 'Using == to compare passwords is vulnerable to timing attacks.',
|
|
952
|
+
fix: { suggestion: 'Use ActiveSupport::SecurityUtils.secure_compare or bcrypt.' },
|
|
953
|
+
check({ files }) {
|
|
954
|
+
return checkRuby(this, files,
|
|
955
|
+
/password\s*==\s*/,
|
|
956
|
+
);
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
|
|
960
|
+
// SEC-RUBY-055: Hardcoded password in source
|
|
961
|
+
{
|
|
962
|
+
id: 'SEC-RUBY-055',
|
|
963
|
+
category: 'security',
|
|
964
|
+
severity: 'critical',
|
|
965
|
+
confidence: 'likely',
|
|
966
|
+
title: 'Hardcoded Password',
|
|
967
|
+
description: 'Hardcoded passwords in source code can be extracted by attackers.',
|
|
968
|
+
fix: { suggestion: 'Use environment variables or a secrets manager for passwords.' },
|
|
969
|
+
check({ files }) {
|
|
970
|
+
return checkRuby(this, files,
|
|
971
|
+
/(?:password|passwd)\s*=\s*["'][^"']{4,}["']/,
|
|
972
|
+
);
|
|
973
|
+
},
|
|
974
|
+
},
|
|
975
|
+
|
|
976
|
+
// ===========================================================================
|
|
977
|
+
// ERB / Slim Templates (056–060)
|
|
978
|
+
// ===========================================================================
|
|
979
|
+
|
|
980
|
+
// SEC-RUBY-056: Unescaped ERB output
|
|
981
|
+
{
|
|
982
|
+
id: 'SEC-RUBY-056',
|
|
983
|
+
category: 'security',
|
|
984
|
+
severity: 'high',
|
|
985
|
+
confidence: 'likely',
|
|
986
|
+
title: 'Unescaped ERB Output Tag',
|
|
987
|
+
description: 'Using <%== or raw in ERB templates outputs unescaped HTML, enabling XSS.',
|
|
988
|
+
fix: { suggestion: 'Use <%= which auto-escapes output, or sanitize() for HTML content.' },
|
|
989
|
+
check({ files }) {
|
|
990
|
+
return checkRuby(this, files,
|
|
991
|
+
/<%==\s/,
|
|
992
|
+
);
|
|
993
|
+
},
|
|
994
|
+
},
|
|
995
|
+
|
|
996
|
+
// SEC-RUBY-057: ERB template with direct params access
|
|
997
|
+
{
|
|
998
|
+
id: 'SEC-RUBY-057',
|
|
999
|
+
category: 'security',
|
|
1000
|
+
severity: 'high',
|
|
1001
|
+
confidence: 'likely',
|
|
1002
|
+
title: 'Direct params Access in Template',
|
|
1003
|
+
description: 'Accessing params directly in templates bypasses controller-level sanitization.',
|
|
1004
|
+
fix: { suggestion: 'Pass sanitized data from the controller via instance variables.' },
|
|
1005
|
+
check({ files }) {
|
|
1006
|
+
return checkRuby(this, files,
|
|
1007
|
+
/<%=.*params\[/,
|
|
1008
|
+
);
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
|
|
1012
|
+
// SEC-RUBY-058: Slim template with unescaped output
|
|
1013
|
+
{
|
|
1014
|
+
id: 'SEC-RUBY-058',
|
|
1015
|
+
category: 'security',
|
|
1016
|
+
severity: 'high',
|
|
1017
|
+
confidence: 'likely',
|
|
1018
|
+
title: 'Unescaped Slim Output',
|
|
1019
|
+
description: 'Using == in Slim templates outputs unescaped HTML, enabling XSS.',
|
|
1020
|
+
fix: { suggestion: 'Use = for escaped output in Slim templates.' },
|
|
1021
|
+
check({ files }) {
|
|
1022
|
+
return checkRuby(this, files,
|
|
1023
|
+
/^\s*==\s+.*params/,
|
|
1024
|
+
);
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
|
|
1028
|
+
// SEC-RUBY-059: render with user-controlled template
|
|
1029
|
+
{
|
|
1030
|
+
id: 'SEC-RUBY-059',
|
|
1031
|
+
category: 'security',
|
|
1032
|
+
severity: 'critical',
|
|
1033
|
+
confidence: 'likely',
|
|
1034
|
+
title: 'Template Injection via render with User Input',
|
|
1035
|
+
description: 'Passing user input to render() template name allows arbitrary template rendering.',
|
|
1036
|
+
fix: { suggestion: 'Whitelist allowed template names; never pass params directly to render.' },
|
|
1037
|
+
check({ files }) {
|
|
1038
|
+
return checkRuby(this, files,
|
|
1039
|
+
/render\s+params\[/,
|
|
1040
|
+
);
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
// SEC-RUBY-060: Haml unescaped output
|
|
1045
|
+
{
|
|
1046
|
+
id: 'SEC-RUBY-060',
|
|
1047
|
+
category: 'security',
|
|
1048
|
+
severity: 'high',
|
|
1049
|
+
confidence: 'likely',
|
|
1050
|
+
title: 'Unescaped Haml Output',
|
|
1051
|
+
description: 'Using != in Haml templates outputs unescaped HTML, enabling XSS.',
|
|
1052
|
+
fix: { suggestion: 'Use = for auto-escaped output in Haml templates.' },
|
|
1053
|
+
check({ files }) {
|
|
1054
|
+
return checkRuby(this, files,
|
|
1055
|
+
/!=\s+.*params/,
|
|
1056
|
+
);
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
1059
|
+
];
|
|
1060
|
+
|
|
1061
|
+
export default rules;
|