ariadna 1.3.1 → 2.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.
- checksums.yaml +4 -4
- data/ariadna.gemspec +0 -1
- data/data/agents/ariadna-codebase-mapper.md +34 -722
- data/data/agents/ariadna-debugger.md +44 -1139
- data/data/agents/ariadna-executor.md +75 -396
- data/data/agents/ariadna-planner.md +78 -1215
- data/data/agents/ariadna-roadmapper.md +55 -582
- data/data/agents/ariadna-verifier.md +60 -702
- data/data/ariadna/templates/config.json +8 -33
- data/data/ariadna/workflows/debug.md +28 -0
- data/data/ariadna/workflows/execute-phase.md +31 -513
- data/data/ariadna/workflows/map-codebase.md +20 -319
- data/data/ariadna/workflows/new-milestone.md +20 -365
- data/data/ariadna/workflows/new-project.md +19 -880
- data/data/ariadna/workflows/plan-phase.md +24 -443
- data/data/ariadna/workflows/progress.md +20 -376
- data/data/ariadna/workflows/quick.md +19 -221
- data/data/ariadna/workflows/roadmap-ops.md +28 -0
- data/data/ariadna/workflows/verify-work.md +23 -560
- data/data/commands/ariadna/add-phase.md +11 -22
- data/data/commands/ariadna/debug.md +11 -143
- data/data/commands/ariadna/execute-phase.md +12 -30
- data/data/commands/ariadna/insert-phase.md +7 -14
- data/data/commands/ariadna/map-codebase.md +16 -49
- data/data/commands/ariadna/new-milestone.md +12 -25
- data/data/commands/ariadna/new-project.md +22 -26
- data/data/commands/ariadna/plan-phase.md +13 -22
- data/data/commands/ariadna/progress.md +16 -6
- data/data/commands/ariadna/quick.md +9 -11
- data/data/commands/ariadna/remove-phase.md +9 -12
- data/data/commands/ariadna/verify-work.md +14 -19
- data/data/skills/rails-backend/API.md +138 -0
- data/data/skills/rails-backend/CONTROLLERS.md +154 -0
- data/data/skills/rails-backend/JOBS.md +132 -0
- data/data/skills/rails-backend/MODELS.md +213 -0
- data/data/skills/rails-backend/SKILL.md +169 -0
- data/data/skills/rails-frontend/ASSETS.md +154 -0
- data/data/skills/rails-frontend/COMPONENTS.md +253 -0
- data/data/skills/rails-frontend/SKILL.md +187 -0
- data/data/skills/rails-frontend/VIEWS.md +168 -0
- data/data/skills/rails-performance/PROFILING.md +106 -0
- data/data/skills/rails-performance/SKILL.md +217 -0
- data/data/skills/rails-security/AUDIT.md +118 -0
- data/data/skills/rails-security/SKILL.md +422 -0
- data/data/skills/rails-testing/FIXTURES.md +78 -0
- data/data/skills/rails-testing/SKILL.md +160 -0
- data/data/skills/rails-testing/SYSTEM-TESTS.md +73 -0
- data/lib/ariadna/installer.rb +11 -15
- data/lib/ariadna/tools/cli.rb +0 -12
- data/lib/ariadna/tools/config_manager.rb +10 -72
- data/lib/ariadna/tools/frontmatter.rb +23 -1
- data/lib/ariadna/tools/init.rb +201 -401
- data/lib/ariadna/tools/model_profiles.rb +6 -14
- data/lib/ariadna/tools/phase_manager.rb +1 -10
- data/lib/ariadna/tools/state_manager.rb +170 -451
- data/lib/ariadna/tools/template_filler.rb +4 -12
- data/lib/ariadna/tools/verification.rb +21 -399
- data/lib/ariadna/uninstaller.rb +9 -0
- data/lib/ariadna/version.rb +1 -1
- metadata +20 -91
- data/data/agents/ariadna-backend-executor.md +0 -261
- data/data/agents/ariadna-frontend-executor.md +0 -259
- data/data/agents/ariadna-integration-checker.md +0 -418
- data/data/agents/ariadna-phase-researcher.md +0 -469
- data/data/agents/ariadna-plan-checker.md +0 -622
- data/data/agents/ariadna-project-researcher.md +0 -618
- data/data/agents/ariadna-research-synthesizer.md +0 -236
- data/data/agents/ariadna-test-executor.md +0 -266
- data/data/ariadna/references/checkpoints.md +0 -772
- data/data/ariadna/references/continuation-format.md +0 -249
- data/data/ariadna/references/decimal-phase-calculation.md +0 -65
- data/data/ariadna/references/git-integration.md +0 -248
- data/data/ariadna/references/git-planning-commit.md +0 -38
- data/data/ariadna/references/model-profile-resolution.md +0 -32
- data/data/ariadna/references/model-profiles.md +0 -73
- data/data/ariadna/references/phase-argument-parsing.md +0 -61
- data/data/ariadna/references/planning-config.md +0 -194
- data/data/ariadna/references/questioning.md +0 -153
- data/data/ariadna/references/rails-conventions.md +0 -416
- data/data/ariadna/references/tdd.md +0 -267
- data/data/ariadna/references/ui-brand.md +0 -160
- data/data/ariadna/references/verification-patterns.md +0 -853
- data/data/ariadna/templates/codebase/architecture.md +0 -481
- data/data/ariadna/templates/codebase/concerns.md +0 -380
- data/data/ariadna/templates/codebase/conventions.md +0 -434
- data/data/ariadna/templates/codebase/integrations.md +0 -328
- data/data/ariadna/templates/codebase/stack.md +0 -189
- data/data/ariadna/templates/codebase/structure.md +0 -418
- data/data/ariadna/templates/codebase/testing.md +0 -606
- data/data/ariadna/templates/context.md +0 -283
- data/data/ariadna/templates/continue-here.md +0 -78
- data/data/ariadna/templates/debug-subagent-prompt.md +0 -91
- data/data/ariadna/templates/phase-prompt.md +0 -609
- data/data/ariadna/templates/planner-subagent-prompt.md +0 -117
- data/data/ariadna/templates/research-project/ARCHITECTURE.md +0 -439
- data/data/ariadna/templates/research-project/FEATURES.md +0 -168
- data/data/ariadna/templates/research-project/PITFALLS.md +0 -406
- data/data/ariadna/templates/research-project/STACK.md +0 -251
- data/data/ariadna/templates/research-project/SUMMARY.md +0 -247
- data/data/ariadna/templates/state.md +0 -176
- data/data/ariadna/templates/summary-complex.md +0 -59
- data/data/ariadna/templates/summary-minimal.md +0 -41
- data/data/ariadna/templates/summary-standard.md +0 -48
- data/data/ariadna/templates/user-setup.md +0 -310
- data/data/ariadna/workflows/add-phase.md +0 -111
- data/data/ariadna/workflows/add-todo.md +0 -157
- data/data/ariadna/workflows/audit-milestone.md +0 -241
- data/data/ariadna/workflows/check-todos.md +0 -176
- data/data/ariadna/workflows/complete-milestone.md +0 -644
- data/data/ariadna/workflows/diagnose-issues.md +0 -219
- data/data/ariadna/workflows/discovery-phase.md +0 -289
- data/data/ariadna/workflows/discuss-phase.md +0 -408
- data/data/ariadna/workflows/execute-plan.md +0 -448
- data/data/ariadna/workflows/help.md +0 -470
- data/data/ariadna/workflows/insert-phase.md +0 -129
- data/data/ariadna/workflows/list-phase-assumptions.md +0 -178
- data/data/ariadna/workflows/pause-work.md +0 -122
- data/data/ariadna/workflows/plan-milestone-gaps.md +0 -256
- data/data/ariadna/workflows/remove-phase.md +0 -154
- data/data/ariadna/workflows/research-phase.md +0 -74
- data/data/ariadna/workflows/resume-project.md +0 -306
- data/data/ariadna/workflows/set-profile.md +0 -80
- data/data/ariadna/workflows/settings.md +0 -145
- data/data/ariadna/workflows/transition.md +0 -493
- data/data/ariadna/workflows/update.md +0 -212
- data/data/ariadna/workflows/verify-phase.md +0 -226
- data/data/commands/ariadna/add-todo.md +0 -42
- data/data/commands/ariadna/audit-milestone.md +0 -42
- data/data/commands/ariadna/check-todos.md +0 -41
- data/data/commands/ariadna/complete-milestone.md +0 -136
- data/data/commands/ariadna/discuss-phase.md +0 -86
- data/data/commands/ariadna/help.md +0 -22
- data/data/commands/ariadna/list-phase-assumptions.md +0 -50
- data/data/commands/ariadna/pause-work.md +0 -35
- data/data/commands/ariadna/plan-milestone-gaps.md +0 -40
- data/data/commands/ariadna/reapply-patches.md +0 -110
- data/data/commands/ariadna/research-phase.md +0 -187
- data/data/commands/ariadna/resume-work.md +0 -40
- data/data/commands/ariadna/set-profile.md +0 -34
- data/data/commands/ariadna/settings.md +0 -36
- data/data/commands/ariadna/update.md +0 -37
- data/data/guides/backend.md +0 -3069
- data/data/guides/frontend.md +0 -1479
- data/data/guides/performance.md +0 -1193
- data/data/guides/security.md +0 -1522
- data/data/guides/style-guide.md +0 -1091
- data/data/guides/testing.md +0 -504
- data/data/templates.md +0 -94
data/data/guides/security.md
DELETED
|
@@ -1,1522 +0,0 @@
|
|
|
1
|
-
# Security Guide for Rails Code Review
|
|
2
|
-
|
|
3
|
-
**Agent-Oriented Security Checklist for Automated Code Verification**
|
|
4
|
-
|
|
5
|
-
This guide provides a structured checklist for verifying code security after each development phase. Each section contains named CHECK items with severity levels, file glob patterns, and UNSAFE/SAFE code examples so an agent can systematically scan changed files and report findings.
|
|
6
|
-
|
|
7
|
-
Unlike informational security documentation, this guide is **action-oriented** — every check tells you what to look for, where to look, and how to fix it.
|
|
8
|
-
|
|
9
|
-
**Related guides:**
|
|
10
|
-
- [Backend Patterns](backend.md) — Architecture, models, controllers, jobs, style guide
|
|
11
|
-
- [Frontend Patterns](frontend.md) — Presenter pattern, view layer conventions
|
|
12
|
-
- [Testing Patterns](testing.md) — Testing philosophy, model/controller/job test patterns
|
|
13
|
-
|
|
14
|
-
## Table of Contents
|
|
15
|
-
|
|
16
|
-
- [Part 1: Input Handling & Injection](#part-1-input-handling--injection)
|
|
17
|
-
- [1.1 SQL Injection](#11-sql-injection)
|
|
18
|
-
- [1.2 Cross-Site Scripting / XSS](#12-cross-site-scripting--xss)
|
|
19
|
-
- [1.3 Command Injection](#13-command-injection)
|
|
20
|
-
- [1.4 Regular Expression Safety](#14-regular-expression-safety)
|
|
21
|
-
- [Part 2: Request Integrity](#part-2-request-integrity)
|
|
22
|
-
- [2.1 CSRF Protection](#21-csrf-protection)
|
|
23
|
-
- [2.2 Mass Assignment & Strong Parameters](#22-mass-assignment--strong-parameters)
|
|
24
|
-
- [2.3 Redirect Security](#23-redirect-security)
|
|
25
|
-
- [Part 3: Authentication & Authorization](#part-3-authentication--authorization)
|
|
26
|
-
- [3.1 Authentication](#31-authentication)
|
|
27
|
-
- [3.2 Authorization & IDOR](#32-authorization--idor)
|
|
28
|
-
- [3.3 Session Security](#33-session-security)
|
|
29
|
-
- [Part 4: Data Protection](#part-4-data-protection)
|
|
30
|
-
- [4.1 Secrets Management](#41-secrets-management)
|
|
31
|
-
- [4.2 Logging & Parameter Filtering](#42-logging--parameter-filtering)
|
|
32
|
-
- [4.3 File Upload Security](#43-file-upload-security)
|
|
33
|
-
- [Part 5: Infrastructure Security](#part-5-infrastructure-security)
|
|
34
|
-
- [5.1 HTTP Security Headers](#51-http-security-headers)
|
|
35
|
-
- [5.2 API Security](#52-api-security)
|
|
36
|
-
- [5.3 Dependency Auditing](#53-dependency-auditing)
|
|
37
|
-
- [Part 6: Security Verification Checklist](#part-6-security-verification-checklist)
|
|
38
|
-
- [6.1 Agent Check Protocol](#61-agent-check-protocol)
|
|
39
|
-
- [6.2 Quick-Reference Checklist](#62-quick-reference-checklist)
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
# Part 1: Input Handling & Injection
|
|
44
|
-
|
|
45
|
-
Injection attacks exploit untrusted input that reaches interpreters (SQL, HTML, shell) without proper sanitization. These checks cover the most common injection vectors in Rails applications.
|
|
46
|
-
|
|
47
|
-
## 1.1 SQL Injection
|
|
48
|
-
|
|
49
|
-
SQL injection occurs when user input is interpolated directly into SQL queries. Rails' ActiveRecord API is safe by default, but raw SQL and string interpolation bypass those protections.
|
|
50
|
-
|
|
51
|
-
### CHECK 1.1a: No string interpolation in SQL
|
|
52
|
-
|
|
53
|
-
> **What to look for:** String interpolation (`#{}`) or concatenation (`+`) inside `.where()`, `.order()`, `.joins()`, `.group()`, `.having()`, `.from()`, `.select()`, `.pluck()`, or raw SQL strings
|
|
54
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`
|
|
55
|
-
> **Severity:** Critical
|
|
56
|
-
|
|
57
|
-
**UNSAFE:**
|
|
58
|
-
|
|
59
|
-
```ruby
|
|
60
|
-
User.where("name = '#{params[:name]}'")
|
|
61
|
-
User.where("role = '" + params[:role] + "'")
|
|
62
|
-
User.order("#{params[:sort]} #{params[:direction]}")
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**SAFE:**
|
|
66
|
-
|
|
67
|
-
```ruby
|
|
68
|
-
User.where(name: params[:name])
|
|
69
|
-
User.where("name = ?", params[:name])
|
|
70
|
-
User.order(Arel.sql("name")) # only for known-safe static strings
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### CHECK 1.1b: Parameterized raw SQL
|
|
74
|
-
|
|
75
|
-
> **What to look for:** `execute()`, `exec_query()`, `find_by_sql()`, `select_all()` calls without bind parameters
|
|
76
|
-
> **Where to look:** `app/models/**/*.rb`, `lib/**/*.rb`
|
|
77
|
-
> **Severity:** Critical
|
|
78
|
-
|
|
79
|
-
**UNSAFE:**
|
|
80
|
-
|
|
81
|
-
```ruby
|
|
82
|
-
ActiveRecord::Base.connection.execute(
|
|
83
|
-
"UPDATE users SET name = '#{name}' WHERE id = #{id}"
|
|
84
|
-
)
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
**SAFE:**
|
|
88
|
-
|
|
89
|
-
```ruby
|
|
90
|
-
ActiveRecord::Base.connection.exec_query(
|
|
91
|
-
"UPDATE users SET name = $1 WHERE id = $2",
|
|
92
|
-
"SQL", [name, id]
|
|
93
|
-
)
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### CHECK 1.1c: Safe column names in order/group
|
|
97
|
-
|
|
98
|
-
> **What to look for:** User-controlled values passed to `.order()` or `.group()` without allowlist validation
|
|
99
|
-
> **Where to look:** `app/controllers/**/*.rb`, `app/models/**/*.rb`
|
|
100
|
-
> **Severity:** High
|
|
101
|
-
|
|
102
|
-
**UNSAFE:**
|
|
103
|
-
|
|
104
|
-
```ruby
|
|
105
|
-
scope :sorted_by, ->(column) { order(column) }
|
|
106
|
-
|
|
107
|
-
# Controller
|
|
108
|
-
@users = User.order(params[:sort])
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**SAFE:**
|
|
112
|
-
|
|
113
|
-
```ruby
|
|
114
|
-
ALLOWED_SORT_COLUMNS = %w[name created_at updated_at].freeze
|
|
115
|
-
|
|
116
|
-
scope :sorted_by, ->(column) do
|
|
117
|
-
if ALLOWED_SORT_COLUMNS.include?(column)
|
|
118
|
-
order(column)
|
|
119
|
-
else
|
|
120
|
-
order(:created_at)
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### CHECK 1.1d: No unscoped find for user-facing lookups
|
|
126
|
-
|
|
127
|
-
> **What to look for:** `Model.find(params[:id])` without tenant or ownership scoping
|
|
128
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
129
|
-
> **Severity:** High
|
|
130
|
-
|
|
131
|
-
**UNSAFE:**
|
|
132
|
-
|
|
133
|
-
```ruby
|
|
134
|
-
def show
|
|
135
|
-
@card = Card.find(params[:id])
|
|
136
|
-
end
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
**SAFE:**
|
|
140
|
-
|
|
141
|
-
```ruby
|
|
142
|
-
def show
|
|
143
|
-
@card = Current.user.accessible_cards.find_by!(number: params[:id])
|
|
144
|
-
end
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
---
|
|
148
|
-
|
|
149
|
-
## 1.2 Cross-Site Scripting / XSS
|
|
150
|
-
|
|
151
|
-
XSS attacks inject malicious scripts into pages viewed by other users. Rails auto-escapes output in ERB by default, but several patterns bypass this protection.
|
|
152
|
-
|
|
153
|
-
### CHECK 1.2a: No raw/html_safe on user input
|
|
154
|
-
|
|
155
|
-
> **What to look for:** `.html_safe`, `raw()`, or `<%== %>` applied to user-supplied data or database content that originates from user input
|
|
156
|
-
> **Where to look:** `app/views/**/*.erb`, `app/helpers/**/*.rb`, `app/models/**/*.rb`
|
|
157
|
-
> **Severity:** Critical
|
|
158
|
-
|
|
159
|
-
**UNSAFE:**
|
|
160
|
-
|
|
161
|
-
```erb
|
|
162
|
-
<%= params[:query].html_safe %>
|
|
163
|
-
<%= raw(@user.bio) %>
|
|
164
|
-
<%== comment.body %>
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
**SAFE:**
|
|
168
|
-
|
|
169
|
-
```erb
|
|
170
|
-
<%= params[:query] %>
|
|
171
|
-
<%= sanitize(@user.bio) %>
|
|
172
|
-
<%= comment.body %>
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### CHECK 1.2b: Sanitized rich text output
|
|
176
|
-
|
|
177
|
-
> **What to look for:** Rich text or markdown rendered without sanitization; use of `.to_s` on ActionText content bypassing built-in sanitization
|
|
178
|
-
> **Where to look:** `app/views/**/*.erb`, `app/helpers/**/*.rb`
|
|
179
|
-
> **Severity:** High
|
|
180
|
-
|
|
181
|
-
**UNSAFE:**
|
|
182
|
-
|
|
183
|
-
```erb
|
|
184
|
-
<%= @card.description.to_s.html_safe %>
|
|
185
|
-
<div><%= raw(markdown_to_html(@post.body)) %></div>
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
**SAFE:**
|
|
189
|
-
|
|
190
|
-
```erb
|
|
191
|
-
<%= @card.description %>
|
|
192
|
-
<div><%= sanitize(markdown_to_html(@post.body)) %></div>
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### CHECK 1.2c: Safe link_to href values
|
|
196
|
-
|
|
197
|
-
> **What to look for:** `link_to` or `<a href>` where the URL comes from user input or database fields without protocol validation
|
|
198
|
-
> **Where to look:** `app/views/**/*.erb`, `app/helpers/**/*.rb`
|
|
199
|
-
> **Severity:** High
|
|
200
|
-
|
|
201
|
-
**UNSAFE:**
|
|
202
|
-
|
|
203
|
-
```erb
|
|
204
|
-
<%= link_to "Website", @user.website_url %>
|
|
205
|
-
<a href="<%= params[:return_to] %>">Back</a>
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**SAFE:**
|
|
209
|
-
|
|
210
|
-
```erb
|
|
211
|
-
<%= link_to "Website", @user.website_url if @user.website_url&.match?(%r{\Ahttps?://}) %>
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### CHECK 1.2d: JSON output escaped in script tags
|
|
215
|
-
|
|
216
|
-
> **What to look for:** Inline `<script>` blocks that interpolate Ruby data without `json_escape` or the `j` helper
|
|
217
|
-
> **Where to look:** `app/views/**/*.erb`
|
|
218
|
-
> **Severity:** High
|
|
219
|
-
|
|
220
|
-
**UNSAFE:**
|
|
221
|
-
|
|
222
|
-
```erb
|
|
223
|
-
<script>
|
|
224
|
-
var data = <%= @data.to_json %>;
|
|
225
|
-
</script>
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
**SAFE:**
|
|
229
|
-
|
|
230
|
-
```erb
|
|
231
|
-
<script>
|
|
232
|
-
var data = <%= json_escape(@data.to_json) %>;
|
|
233
|
-
</script>
|
|
234
|
-
|
|
235
|
-
<!-- Or better: use data attributes -->
|
|
236
|
-
<div data-config="<%= @data.to_json %>">
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
### CHECK 1.2e: Content-Type set for non-HTML responses
|
|
240
|
-
|
|
241
|
-
> **What to look for:** Controller actions rendering user-supplied content (CSV, text, SVG) without setting an explicit Content-Type
|
|
242
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
243
|
-
> **Severity:** Medium
|
|
244
|
-
|
|
245
|
-
**UNSAFE:**
|
|
246
|
-
|
|
247
|
-
```ruby
|
|
248
|
-
def export
|
|
249
|
-
render plain: @data.to_csv
|
|
250
|
-
end
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
**SAFE:**
|
|
254
|
-
|
|
255
|
-
```ruby
|
|
256
|
-
def export
|
|
257
|
-
send_data @data.to_csv, type: "text/csv", disposition: "attachment"
|
|
258
|
-
end
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
---
|
|
262
|
-
|
|
263
|
-
## 1.3 Command Injection
|
|
264
|
-
|
|
265
|
-
Command injection occurs when user input reaches shell functions without sanitization.
|
|
266
|
-
|
|
267
|
-
### CHECK 1.3a: No user input in shell calls
|
|
268
|
-
|
|
269
|
-
> **What to look for:** `system()`, backticks, `%x()`, `IO.popen()`, `Open3.capture2()`, `Open3.capture3()`, or `Kernel.spawn()` with string interpolation or concatenation of user input
|
|
270
|
-
> **Where to look:** `app/**/*.rb`, `lib/**/*.rb`
|
|
271
|
-
> **Severity:** Critical
|
|
272
|
-
|
|
273
|
-
**UNSAFE:**
|
|
274
|
-
|
|
275
|
-
```ruby
|
|
276
|
-
# Vulnerable: single-string form passes through shell
|
|
277
|
-
system("convert #{params[:file]} output.png")
|
|
278
|
-
result = `grep -r #{params[:query]} /data`
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
**SAFE:**
|
|
282
|
-
|
|
283
|
-
```ruby
|
|
284
|
-
# Safe: array form bypasses shell interpretation
|
|
285
|
-
system("convert", uploaded_file.path, "output.png")
|
|
286
|
-
stdout, status = Open3.capture2("grep", "-r", query, "/data")
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### CHECK 1.3b: Shellwords escaping for unavoidable shell use
|
|
290
|
-
|
|
291
|
-
> **What to look for:** Cases where shell invocation with a single string is unavoidable — verify `Shellwords.escape()` or array form is used
|
|
292
|
-
> **Where to look:** `app/**/*.rb`, `lib/**/*.rb`
|
|
293
|
-
> **Severity:** High
|
|
294
|
-
|
|
295
|
-
**UNSAFE:**
|
|
296
|
-
|
|
297
|
-
```ruby
|
|
298
|
-
system("tar -czf archive.tar.gz #{directory}")
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
**SAFE:**
|
|
302
|
-
|
|
303
|
-
```ruby
|
|
304
|
-
# Prefer array form:
|
|
305
|
-
system("tar", "-czf", "archive.tar.gz", directory)
|
|
306
|
-
# If shell string is required:
|
|
307
|
-
system("tar -czf archive.tar.gz #{Shellwords.escape(directory)}")
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
---
|
|
311
|
-
|
|
312
|
-
## 1.4 Regular Expression Safety
|
|
313
|
-
|
|
314
|
-
Ruby's `^` and `$` anchors match line boundaries, not string boundaries. This can allow bypass of regex-based validations.
|
|
315
|
-
|
|
316
|
-
### CHECK 1.4a: Use \A and \z instead of ^ and $
|
|
317
|
-
|
|
318
|
-
> **What to look for:** Regex validations using `^` (start of line) and `$` (end of line) instead of `\A` (start of string) and `\z` (end of string)
|
|
319
|
-
> **Where to look:** `app/models/**/*.rb`, `app/validators/**/*.rb`
|
|
320
|
-
> **Severity:** High
|
|
321
|
-
|
|
322
|
-
**UNSAFE:**
|
|
323
|
-
|
|
324
|
-
```ruby
|
|
325
|
-
validates :slug, format: { with: /^[a-z0-9-]+$/ }
|
|
326
|
-
# Allows "valid-slug\n<script>alert(1)</script>"
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
**SAFE:**
|
|
330
|
-
|
|
331
|
-
```ruby
|
|
332
|
-
validates :slug, format: { with: /\A[a-z0-9-]+\z/ }
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
---
|
|
336
|
-
|
|
337
|
-
# Part 2: Request Integrity
|
|
338
|
-
|
|
339
|
-
These checks verify that requests are authentic, properly scoped, and cannot be manipulated to perform unintended actions.
|
|
340
|
-
|
|
341
|
-
## 2.1 CSRF Protection
|
|
342
|
-
|
|
343
|
-
Cross-Site Request Forgery tricks authenticated users into submitting unwanted requests. Rails includes CSRF protection by default, but it can be accidentally disabled.
|
|
344
|
-
|
|
345
|
-
### CHECK 2.1a: CSRF protection enabled
|
|
346
|
-
|
|
347
|
-
> **What to look for:** `skip_before_action :verify_authenticity_token` or `protect_from_forgery` disabled in controllers that handle browser requests
|
|
348
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
349
|
-
> **Severity:** Critical
|
|
350
|
-
|
|
351
|
-
**UNSAFE:**
|
|
352
|
-
|
|
353
|
-
```ruby
|
|
354
|
-
class PaymentsController < ApplicationController
|
|
355
|
-
skip_before_action :verify_authenticity_token
|
|
356
|
-
end
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
**SAFE:**
|
|
360
|
-
|
|
361
|
-
```ruby
|
|
362
|
-
class PaymentsController < ApplicationController
|
|
363
|
-
# CSRF protection inherited from ApplicationController
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
# Only skip for stateless API endpoints with token auth:
|
|
367
|
-
class Api::V1::BaseController < ActionController::API
|
|
368
|
-
# ActionController::API does not include CSRF (correct for token-auth APIs)
|
|
369
|
-
end
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
### CHECK 2.1b: Authenticity token in forms
|
|
373
|
-
|
|
374
|
-
> **What to look for:** Hand-crafted `<form>` tags without `<%= csrf_meta_tags %>` in layout or `authenticity_token` in the form; JavaScript fetch/XHR without CSRF token header
|
|
375
|
-
> **Where to look:** `app/views/**/*.erb`, `app/javascript/**/*.js`
|
|
376
|
-
> **Severity:** High
|
|
377
|
-
|
|
378
|
-
**UNSAFE:**
|
|
379
|
-
|
|
380
|
-
```html
|
|
381
|
-
<form action="/transfer" method="post">
|
|
382
|
-
<input name="amount" value="1000">
|
|
383
|
-
</form>
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
**SAFE:**
|
|
387
|
-
|
|
388
|
-
```erb
|
|
389
|
-
<%= form_with url: transfer_path do |f| %>
|
|
390
|
-
<%= f.number_field :amount %>
|
|
391
|
-
<% end %>
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
### CHECK 2.1c: State-changing actions use non-GET methods
|
|
395
|
-
|
|
396
|
-
> **What to look for:** Routes that perform state changes (create, update, delete) mapped to GET requests
|
|
397
|
-
> **Where to look:** `config/routes.rb`
|
|
398
|
-
> **Severity:** High
|
|
399
|
-
|
|
400
|
-
**UNSAFE:**
|
|
401
|
-
|
|
402
|
-
```ruby
|
|
403
|
-
get "users/:id/delete", to: "users#destroy"
|
|
404
|
-
get "posts/:id/publish", to: "posts#publish"
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
**SAFE:**
|
|
408
|
-
|
|
409
|
-
```ruby
|
|
410
|
-
resources :users, only: [:destroy]
|
|
411
|
-
resource :publication, only: [:create, :destroy]
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
---
|
|
415
|
-
|
|
416
|
-
## 2.2 Mass Assignment & Strong Parameters
|
|
417
|
-
|
|
418
|
-
Mass assignment occurs when user-supplied parameters are passed directly to model create/update methods, allowing attackers to set unintended attributes.
|
|
419
|
-
|
|
420
|
-
### CHECK 2.2a: Strong parameters in controllers
|
|
421
|
-
|
|
422
|
-
> **What to look for:** `params.permit!`, `Model.create(params)`, `Model.update(params)`, or `Model.new(params)` without strong parameter filtering
|
|
423
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
424
|
-
> **Severity:** Critical
|
|
425
|
-
|
|
426
|
-
**UNSAFE:**
|
|
427
|
-
|
|
428
|
-
```ruby
|
|
429
|
-
def create
|
|
430
|
-
@user = User.create(params[:user])
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
def update
|
|
434
|
-
@user.update(params.permit!)
|
|
435
|
-
end
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
**SAFE:**
|
|
439
|
-
|
|
440
|
-
```ruby
|
|
441
|
-
def create
|
|
442
|
-
@user = User.create(user_params)
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
private
|
|
446
|
-
def user_params
|
|
447
|
-
params.require(:user).permit(:name, :email)
|
|
448
|
-
end
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
### CHECK 2.2b: No admin/role attributes in permit lists
|
|
452
|
-
|
|
453
|
-
> **What to look for:** Sensitive attributes (`admin`, `role`, `account_id`, `user_id`, `verified`, `approved`) in strong parameter permit lists
|
|
454
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
455
|
-
> **Severity:** High
|
|
456
|
-
|
|
457
|
-
**UNSAFE:**
|
|
458
|
-
|
|
459
|
-
```ruby
|
|
460
|
-
def user_params
|
|
461
|
-
params.require(:user).permit(:name, :email, :admin, :role)
|
|
462
|
-
end
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
**SAFE:**
|
|
466
|
-
|
|
467
|
-
```ruby
|
|
468
|
-
def user_params
|
|
469
|
-
params.require(:user).permit(:name, :email)
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
# Admin attributes set explicitly with authorization check:
|
|
473
|
-
def promote
|
|
474
|
-
authorize! :manage, @user
|
|
475
|
-
@user.update(role: params[:user][:role])
|
|
476
|
-
end
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### CHECK 2.2c: Nested attributes allowlisted
|
|
480
|
-
|
|
481
|
-
> **What to look for:** `accepts_nested_attributes_for` without corresponding strong parameter scoping, or overly permissive nested attribute configs
|
|
482
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`
|
|
483
|
-
> **Severity:** Medium
|
|
484
|
-
|
|
485
|
-
**UNSAFE:**
|
|
486
|
-
|
|
487
|
-
```ruby
|
|
488
|
-
class Project < ApplicationRecord
|
|
489
|
-
accepts_nested_attributes_for :tasks, allow_destroy: true
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# Controller permits all nested attributes
|
|
493
|
-
def project_params
|
|
494
|
-
params.require(:project).permit!
|
|
495
|
-
end
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
**SAFE:**
|
|
499
|
-
|
|
500
|
-
```ruby
|
|
501
|
-
class Project < ApplicationRecord
|
|
502
|
-
accepts_nested_attributes_for :tasks, allow_destroy: true,
|
|
503
|
-
reject_if: :all_blank
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
def project_params
|
|
507
|
-
params.require(:project).permit(:name,
|
|
508
|
-
tasks_attributes: [:id, :title, :done, :_destroy])
|
|
509
|
-
end
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
---
|
|
513
|
-
|
|
514
|
-
## 2.3 Redirect Security
|
|
515
|
-
|
|
516
|
-
Open redirects allow attackers to send users to malicious sites while appearing to link from a trusted domain.
|
|
517
|
-
|
|
518
|
-
### CHECK 2.3a: No open redirects from user input
|
|
519
|
-
|
|
520
|
-
> **What to look for:** `redirect_to params[:url]`, `redirect_to params[:return_to]`, or any redirect target sourced from user input without validation
|
|
521
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
522
|
-
> **Severity:** High
|
|
523
|
-
|
|
524
|
-
**UNSAFE:**
|
|
525
|
-
|
|
526
|
-
```ruby
|
|
527
|
-
def callback
|
|
528
|
-
redirect_to params[:return_to]
|
|
529
|
-
end
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
**SAFE:**
|
|
533
|
-
|
|
534
|
-
```ruby
|
|
535
|
-
def callback
|
|
536
|
-
redirect_to params[:return_to] if safe_redirect?(params[:return_to])
|
|
537
|
-
end
|
|
538
|
-
|
|
539
|
-
private
|
|
540
|
-
def safe_redirect?(url)
|
|
541
|
-
uri = URI.parse(url.to_s)
|
|
542
|
-
uri.host.nil? || uri.host == request.host
|
|
543
|
-
rescue URI::InvalidURIError
|
|
544
|
-
false
|
|
545
|
-
end
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
### CHECK 2.3b: Redirect allowlists for external URLs
|
|
549
|
-
|
|
550
|
-
> **What to look for:** Redirects to external domains without an explicit allowlist
|
|
551
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
552
|
-
> **Severity:** Medium
|
|
553
|
-
|
|
554
|
-
**UNSAFE:**
|
|
555
|
-
|
|
556
|
-
```ruby
|
|
557
|
-
def sso_callback
|
|
558
|
-
redirect_to params[:redirect_uri], allow_other_host: true
|
|
559
|
-
end
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
**SAFE:**
|
|
563
|
-
|
|
564
|
-
```ruby
|
|
565
|
-
ALLOWED_REDIRECT_HOSTS = %w[accounts.example.com auth.example.com].freeze
|
|
566
|
-
|
|
567
|
-
def sso_callback
|
|
568
|
-
uri = URI.parse(params[:redirect_uri])
|
|
569
|
-
if ALLOWED_REDIRECT_HOSTS.include?(uri.host)
|
|
570
|
-
redirect_to params[:redirect_uri], allow_other_host: true
|
|
571
|
-
else
|
|
572
|
-
redirect_to root_path
|
|
573
|
-
end
|
|
574
|
-
end
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
---
|
|
578
|
-
|
|
579
|
-
# Part 3: Authentication & Authorization
|
|
580
|
-
|
|
581
|
-
These checks verify that users are properly identified and can only access resources they are permitted to use.
|
|
582
|
-
|
|
583
|
-
## 3.1 Authentication
|
|
584
|
-
|
|
585
|
-
### CHECK 3.1a: Authentication required on all controllers
|
|
586
|
-
|
|
587
|
-
> **What to look for:** Controllers that inherit from `ApplicationController` but skip or lack authentication before_action; public-facing controllers without explicit `allow_unauthenticated_access`
|
|
588
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
589
|
-
> **Severity:** Critical
|
|
590
|
-
|
|
591
|
-
**UNSAFE:**
|
|
592
|
-
|
|
593
|
-
```ruby
|
|
594
|
-
class AdminController < ApplicationController
|
|
595
|
-
skip_before_action :authenticate
|
|
596
|
-
# All admin actions now publicly accessible
|
|
597
|
-
end
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
**SAFE:**
|
|
601
|
-
|
|
602
|
-
```ruby
|
|
603
|
-
class AdminController < ApplicationController
|
|
604
|
-
# Inherits authenticate from ApplicationController
|
|
605
|
-
before_action :require_admin
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
# Only skip auth intentionally for public pages:
|
|
609
|
-
class SessionsController < ApplicationController
|
|
610
|
-
allow_unauthenticated_access only: [:new, :create]
|
|
611
|
-
end
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
### CHECK 3.1b: Secure password handling
|
|
615
|
-
|
|
616
|
-
> **What to look for:** Passwords stored in plain text, weak hashing (MD5, SHA1), or custom password hashing instead of `has_secure_password` or bcrypt
|
|
617
|
-
> **Where to look:** `app/models/**/*.rb`, `db/migrate/**/*.rb`
|
|
618
|
-
> **Severity:** Critical
|
|
619
|
-
|
|
620
|
-
**UNSAFE:**
|
|
621
|
-
|
|
622
|
-
```ruby
|
|
623
|
-
class User < ApplicationRecord
|
|
624
|
-
def password=(value)
|
|
625
|
-
self.password_hash = Digest::MD5.hexdigest(value)
|
|
626
|
-
end
|
|
627
|
-
end
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
**SAFE:**
|
|
631
|
-
|
|
632
|
-
```ruby
|
|
633
|
-
class User < ApplicationRecord
|
|
634
|
-
has_secure_password
|
|
635
|
-
# Uses bcrypt via ActiveModel::SecurePassword
|
|
636
|
-
end
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
### CHECK 3.1c: Timing-safe comparisons for tokens
|
|
640
|
-
|
|
641
|
-
> **What to look for:** Token or secret comparison using `==` instead of `ActiveSupport::SecurityUtils.secure_compare`
|
|
642
|
-
> **Where to look:** `app/controllers/**/*.rb`, `app/models/**/*.rb`
|
|
643
|
-
> **Severity:** High
|
|
644
|
-
|
|
645
|
-
**UNSAFE:**
|
|
646
|
-
|
|
647
|
-
```ruby
|
|
648
|
-
def verify_webhook
|
|
649
|
-
if request.headers["X-Signature"] == compute_signature(request.body)
|
|
650
|
-
process_webhook
|
|
651
|
-
end
|
|
652
|
-
end
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
**SAFE:**
|
|
656
|
-
|
|
657
|
-
```ruby
|
|
658
|
-
def verify_webhook
|
|
659
|
-
expected = compute_signature(request.body.read)
|
|
660
|
-
actual = request.headers["X-Signature"].to_s
|
|
661
|
-
if ActiveSupport::SecurityUtils.secure_compare(actual, expected)
|
|
662
|
-
process_webhook
|
|
663
|
-
end
|
|
664
|
-
end
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
### CHECK 3.1d: Rate limiting on authentication endpoints
|
|
668
|
-
|
|
669
|
-
> **What to look for:** Login, password reset, and token verification endpoints without rate limiting
|
|
670
|
-
> **Where to look:** `app/controllers/sessions_controller.rb`, `app/controllers/passwords_controller.rb`, `config/routes.rb`
|
|
671
|
-
> **Severity:** High
|
|
672
|
-
|
|
673
|
-
**UNSAFE:**
|
|
674
|
-
|
|
675
|
-
```ruby
|
|
676
|
-
class SessionsController < ApplicationController
|
|
677
|
-
def create
|
|
678
|
-
if user = User.authenticate_by(email: params[:email], password: params[:password])
|
|
679
|
-
start_session(user)
|
|
680
|
-
end
|
|
681
|
-
end
|
|
682
|
-
end
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
**SAFE:**
|
|
686
|
-
|
|
687
|
-
```ruby
|
|
688
|
-
class SessionsController < ApplicationController
|
|
689
|
-
rate_limit to: 10, within: 3.minutes, only: :create,
|
|
690
|
-
with: -> { redirect_to new_session_path, alert: "Try again later." }
|
|
691
|
-
|
|
692
|
-
def create
|
|
693
|
-
if user = User.authenticate_by(email: params[:email], password: params[:password])
|
|
694
|
-
start_session(user)
|
|
695
|
-
end
|
|
696
|
-
end
|
|
697
|
-
end
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
---
|
|
701
|
-
|
|
702
|
-
## 3.2 Authorization & IDOR
|
|
703
|
-
|
|
704
|
-
Insecure Direct Object Reference (IDOR) occurs when users can access resources belonging to other users or tenants by manipulating identifiers.
|
|
705
|
-
|
|
706
|
-
### CHECK 3.2a: Scoped resource lookups
|
|
707
|
-
|
|
708
|
-
> **What to look for:** `Model.find(params[:id])` without scoping through the current user or account; direct model lookups that bypass tenant isolation
|
|
709
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
710
|
-
> **Severity:** Critical
|
|
711
|
-
|
|
712
|
-
**UNSAFE:**
|
|
713
|
-
|
|
714
|
-
```ruby
|
|
715
|
-
def show
|
|
716
|
-
@board = Board.find(params[:id])
|
|
717
|
-
end
|
|
718
|
-
|
|
719
|
-
def update
|
|
720
|
-
@card = Card.find(params[:card_id])
|
|
721
|
-
@card.update(card_params)
|
|
722
|
-
end
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
**SAFE:**
|
|
726
|
-
|
|
727
|
-
```ruby
|
|
728
|
-
def show
|
|
729
|
-
@board = Current.user.boards.find(params[:id])
|
|
730
|
-
end
|
|
731
|
-
|
|
732
|
-
def update
|
|
733
|
-
@card = Current.user.accessible_cards.find_by!(number: params[:card_id])
|
|
734
|
-
@card.update(card_params)
|
|
735
|
-
end
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
### CHECK 3.2b: Authorization checks on destructive actions
|
|
739
|
-
|
|
740
|
-
> **What to look for:** Create, update, and destroy actions that lack explicit authorization checks beyond authentication
|
|
741
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
742
|
-
> **Severity:** High
|
|
743
|
-
|
|
744
|
-
**UNSAFE:**
|
|
745
|
-
|
|
746
|
-
```ruby
|
|
747
|
-
class BoardsController < ApplicationController
|
|
748
|
-
def destroy
|
|
749
|
-
@board = Current.user.boards.find(params[:id])
|
|
750
|
-
@board.destroy
|
|
751
|
-
end
|
|
752
|
-
end
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
**SAFE:**
|
|
756
|
-
|
|
757
|
-
```ruby
|
|
758
|
-
class BoardsController < ApplicationController
|
|
759
|
-
def destroy
|
|
760
|
-
@board = Current.user.boards.find(params[:id])
|
|
761
|
-
if Current.user.can_administer_board?(@board)
|
|
762
|
-
@board.destroy
|
|
763
|
-
else
|
|
764
|
-
head :forbidden
|
|
765
|
-
end
|
|
766
|
-
end
|
|
767
|
-
end
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
### CHECK 3.2c: Tenant isolation in queries
|
|
771
|
-
|
|
772
|
-
> **What to look for:** Queries that do not scope through `Current.account` or a user's tenant-scoped association; cross-tenant data leakage in joins or subqueries
|
|
773
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`
|
|
774
|
-
> **Severity:** Critical
|
|
775
|
-
|
|
776
|
-
**UNSAFE:**
|
|
777
|
-
|
|
778
|
-
```ruby
|
|
779
|
-
def index
|
|
780
|
-
@users = User.where(role: "admin")
|
|
781
|
-
end
|
|
782
|
-
|
|
783
|
-
class Card < ApplicationRecord
|
|
784
|
-
scope :recent, -> { order(created_at: :desc).limit(10) }
|
|
785
|
-
end
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
**SAFE:**
|
|
789
|
-
|
|
790
|
-
```ruby
|
|
791
|
-
def index
|
|
792
|
-
@users = Current.account.users.where(role: "admin")
|
|
793
|
-
end
|
|
794
|
-
|
|
795
|
-
class Card < ApplicationRecord
|
|
796
|
-
scope :recent, -> {
|
|
797
|
-
where(account: Current.account).order(created_at: :desc).limit(10)
|
|
798
|
-
}
|
|
799
|
-
end
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
### CHECK 3.2d: Nested resource authorization
|
|
803
|
-
|
|
804
|
-
> **What to look for:** Nested resource controllers that verify the parent resource but not the child's association to the parent
|
|
805
|
-
> **Where to look:** `app/controllers/**/*.rb`
|
|
806
|
-
> **Severity:** High
|
|
807
|
-
|
|
808
|
-
**UNSAFE:**
|
|
809
|
-
|
|
810
|
-
```ruby
|
|
811
|
-
class Cards::CommentsController < ApplicationController
|
|
812
|
-
def update
|
|
813
|
-
@card = Current.user.accessible_cards.find_by!(number: params[:card_id])
|
|
814
|
-
@comment = Comment.find(params[:id]) # Any comment, not scoped to card
|
|
815
|
-
@comment.update(comment_params)
|
|
816
|
-
end
|
|
817
|
-
end
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
**SAFE:**
|
|
821
|
-
|
|
822
|
-
```ruby
|
|
823
|
-
class Cards::CommentsController < ApplicationController
|
|
824
|
-
def update
|
|
825
|
-
@card = Current.user.accessible_cards.find_by!(number: params[:card_id])
|
|
826
|
-
@comment = @card.comments.find(params[:id])
|
|
827
|
-
@comment.update(comment_params)
|
|
828
|
-
end
|
|
829
|
-
end
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
---
|
|
833
|
-
|
|
834
|
-
## 3.3 Session Security
|
|
835
|
-
|
|
836
|
-
### CHECK 3.3a: Session regeneration after login
|
|
837
|
-
|
|
838
|
-
> **What to look for:** Authentication flows that do not call `reset_session` before setting the new session to prevent session fixation attacks
|
|
839
|
-
> **Where to look:** `app/controllers/sessions_controller.rb`, `app/models/session.rb`
|
|
840
|
-
> **Severity:** High
|
|
841
|
-
|
|
842
|
-
**UNSAFE:**
|
|
843
|
-
|
|
844
|
-
```ruby
|
|
845
|
-
def create
|
|
846
|
-
if user = User.authenticate_by(email: params[:email], password: params[:password])
|
|
847
|
-
session[:user_id] = user.id
|
|
848
|
-
end
|
|
849
|
-
end
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
**SAFE:**
|
|
853
|
-
|
|
854
|
-
```ruby
|
|
855
|
-
def create
|
|
856
|
-
if user = User.authenticate_by(email: params[:email], password: params[:password])
|
|
857
|
-
reset_session
|
|
858
|
-
session[:user_id] = user.id
|
|
859
|
-
end
|
|
860
|
-
end
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
### CHECK 3.3b: Secure cookie configuration
|
|
864
|
-
|
|
865
|
-
> **What to look for:** Session cookies without `secure`, `httponly`, or `same_site` flags in production
|
|
866
|
-
> **Where to look:** `config/environments/production.rb`, `config/initializers/session_store.rb`
|
|
867
|
-
> **Severity:** High
|
|
868
|
-
|
|
869
|
-
**UNSAFE:**
|
|
870
|
-
|
|
871
|
-
```ruby
|
|
872
|
-
Rails.application.config.session_store :cookie_store,
|
|
873
|
-
key: "_app_session"
|
|
874
|
-
```
|
|
875
|
-
|
|
876
|
-
**SAFE:**
|
|
877
|
-
|
|
878
|
-
```ruby
|
|
879
|
-
Rails.application.config.session_store :cookie_store,
|
|
880
|
-
key: "_app_session",
|
|
881
|
-
secure: Rails.env.production?,
|
|
882
|
-
httponly: true,
|
|
883
|
-
same_site: :lax
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
### CHECK 3.3c: Session expiration configured
|
|
887
|
-
|
|
888
|
-
> **What to look for:** Sessions without expiration or idle timeout; long-lived sessions that never expire
|
|
889
|
-
> **Where to look:** `config/initializers/session_store.rb`, `app/models/session.rb`
|
|
890
|
-
> **Severity:** Medium
|
|
891
|
-
|
|
892
|
-
**UNSAFE:**
|
|
893
|
-
|
|
894
|
-
```ruby
|
|
895
|
-
# No expiration set — session lives forever
|
|
896
|
-
Rails.application.config.session_store :cookie_store,
|
|
897
|
-
key: "_app_session"
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
**SAFE:**
|
|
901
|
-
|
|
902
|
-
```ruby
|
|
903
|
-
Rails.application.config.session_store :cookie_store,
|
|
904
|
-
key: "_app_session",
|
|
905
|
-
expire_after: 12.hours
|
|
906
|
-
```
|
|
907
|
-
|
|
908
|
-
---
|
|
909
|
-
|
|
910
|
-
# Part 4: Data Protection
|
|
911
|
-
|
|
912
|
-
These checks verify that sensitive data is properly managed, filtered from logs, and protected at rest and in transit.
|
|
913
|
-
|
|
914
|
-
## 4.1 Secrets Management
|
|
915
|
-
|
|
916
|
-
### CHECK 4.1a: No hardcoded secrets
|
|
917
|
-
|
|
918
|
-
> **What to look for:** API keys, passwords, tokens, or secret strings hardcoded in source files; credentials in non-encrypted config files
|
|
919
|
-
> **Where to look:** `app/**/*.rb`, `config/**/*.rb`, `lib/**/*.rb`, `config/**/*.yml`
|
|
920
|
-
> **Severity:** Critical
|
|
921
|
-
|
|
922
|
-
**UNSAFE:**
|
|
923
|
-
|
|
924
|
-
```ruby
|
|
925
|
-
class StripeService
|
|
926
|
-
API_KEY = "sk_live_abc123xyz"
|
|
927
|
-
end
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
```yaml
|
|
931
|
-
# config/database.yml
|
|
932
|
-
production:
|
|
933
|
-
password: "super_secret_password"
|
|
934
|
-
```
|
|
935
|
-
|
|
936
|
-
**SAFE:**
|
|
937
|
-
|
|
938
|
-
```ruby
|
|
939
|
-
class StripeService
|
|
940
|
-
API_KEY = Rails.application.credentials.stripe[:api_key]
|
|
941
|
-
end
|
|
942
|
-
```
|
|
943
|
-
|
|
944
|
-
```yaml
|
|
945
|
-
# config/database.yml
|
|
946
|
-
production:
|
|
947
|
-
password: <%= ENV["DATABASE_PASSWORD"] %>
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
### CHECK 4.1b: Credentials encrypted
|
|
951
|
-
|
|
952
|
-
> **What to look for:** Sensitive configuration in unencrypted YAML files or `.env` files committed to version control
|
|
953
|
-
> **Where to look:** `config/**/*.yml`, `.env*`, `.gitignore`
|
|
954
|
-
> **Severity:** High
|
|
955
|
-
|
|
956
|
-
**UNSAFE:**
|
|
957
|
-
|
|
958
|
-
```yaml
|
|
959
|
-
# config/secrets.yml (committed to git)
|
|
960
|
-
production:
|
|
961
|
-
secret_key_base: "abc123..."
|
|
962
|
-
smtp_password: "password123"
|
|
963
|
-
```
|
|
964
|
-
|
|
965
|
-
**SAFE:**
|
|
966
|
-
|
|
967
|
-
```bash
|
|
968
|
-
# Use Rails encrypted credentials
|
|
969
|
-
bin/rails credentials:edit
|
|
970
|
-
|
|
971
|
-
# .gitignore includes:
|
|
972
|
-
# .env
|
|
973
|
-
# config/credentials/*.key
|
|
974
|
-
```
|
|
975
|
-
|
|
976
|
-
### CHECK 4.1c: Secret key base configured
|
|
977
|
-
|
|
978
|
-
> **What to look for:** Missing or weak `secret_key_base` in production; default development secret used in production
|
|
979
|
-
> **Where to look:** `config/environments/production.rb`, `config/credentials.yml.enc`
|
|
980
|
-
> **Severity:** Critical
|
|
981
|
-
|
|
982
|
-
**UNSAFE:**
|
|
983
|
-
|
|
984
|
-
```ruby
|
|
985
|
-
# config/environments/production.rb
|
|
986
|
-
config.secret_key_base = "development_secret_do_not_use"
|
|
987
|
-
```
|
|
988
|
-
|
|
989
|
-
**SAFE:**
|
|
990
|
-
|
|
991
|
-
```ruby
|
|
992
|
-
# config/environments/production.rb
|
|
993
|
-
config.secret_key_base = Rails.application.credentials.secret_key_base
|
|
994
|
-
# Or via environment variable:
|
|
995
|
-
config.secret_key_base = ENV.fetch("SECRET_KEY_BASE")
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
---
|
|
999
|
-
|
|
1000
|
-
## 4.2 Logging & Parameter Filtering
|
|
1001
|
-
|
|
1002
|
-
### CHECK 4.2a: Sensitive parameters filtered
|
|
1003
|
-
|
|
1004
|
-
> **What to look for:** Password, token, credit card, and other sensitive fields not listed in `filter_parameters`
|
|
1005
|
-
> **Where to look:** `config/initializers/filter_parameter_logging.rb`
|
|
1006
|
-
> **Severity:** High
|
|
1007
|
-
|
|
1008
|
-
**UNSAFE:**
|
|
1009
|
-
|
|
1010
|
-
```ruby
|
|
1011
|
-
# Only filtering password — missing other sensitive fields
|
|
1012
|
-
Rails.application.config.filter_parameters += [:password]
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
**SAFE:**
|
|
1016
|
-
|
|
1017
|
-
```ruby
|
|
1018
|
-
Rails.application.config.filter_parameters += [
|
|
1019
|
-
:password, :password_confirmation,
|
|
1020
|
-
:token, :api_key, :secret,
|
|
1021
|
-
:credit_card, :card_number, :cvv,
|
|
1022
|
-
:ssn, :social_security
|
|
1023
|
-
]
|
|
1024
|
-
```
|
|
1025
|
-
|
|
1026
|
-
### CHECK 4.2b: No sensitive data in logs
|
|
1027
|
-
|
|
1028
|
-
> **What to look for:** `Rails.logger`, `logger.info`, `puts`, or `p` statements that output user data, tokens, or PII
|
|
1029
|
-
> **Where to look:** `app/**/*.rb`, `lib/**/*.rb`
|
|
1030
|
-
> **Severity:** High
|
|
1031
|
-
|
|
1032
|
-
**UNSAFE:**
|
|
1033
|
-
|
|
1034
|
-
```ruby
|
|
1035
|
-
Rails.logger.info "User login: #{user.email}, token: #{user.auth_token}"
|
|
1036
|
-
Rails.logger.debug "Payment params: #{params.inspect}"
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
**SAFE:**
|
|
1040
|
-
|
|
1041
|
-
```ruby
|
|
1042
|
-
Rails.logger.info "User login: user_id=#{user.id}"
|
|
1043
|
-
Rails.logger.debug "Payment processed for user_id=#{user.id}"
|
|
1044
|
-
```
|
|
1045
|
-
|
|
1046
|
-
---
|
|
1047
|
-
|
|
1048
|
-
## 4.3 File Upload Security
|
|
1049
|
-
|
|
1050
|
-
### CHECK 4.3a: Content type validation
|
|
1051
|
-
|
|
1052
|
-
> **What to look for:** File uploads accepted without content type validation; relying solely on file extension for type checking
|
|
1053
|
-
> **Where to look:** `app/models/**/*.rb`, `app/controllers/**/*.rb`
|
|
1054
|
-
> **Severity:** High
|
|
1055
|
-
|
|
1056
|
-
**UNSAFE:**
|
|
1057
|
-
|
|
1058
|
-
```ruby
|
|
1059
|
-
class Document < ApplicationRecord
|
|
1060
|
-
has_one_attached :file
|
|
1061
|
-
# No content type validation
|
|
1062
|
-
end
|
|
1063
|
-
```
|
|
1064
|
-
|
|
1065
|
-
**SAFE:**
|
|
1066
|
-
|
|
1067
|
-
```ruby
|
|
1068
|
-
class Document < ApplicationRecord
|
|
1069
|
-
has_one_attached :file
|
|
1070
|
-
|
|
1071
|
-
validates :file, content_type: %w[
|
|
1072
|
-
application/pdf
|
|
1073
|
-
image/png
|
|
1074
|
-
image/jpeg
|
|
1075
|
-
]
|
|
1076
|
-
end
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
### CHECK 4.3b: File size limits
|
|
1080
|
-
|
|
1081
|
-
> **What to look for:** File uploads without size limits; missing `size` validation on ActiveStorage attachments
|
|
1082
|
-
> **Where to look:** `app/models/**/*.rb`
|
|
1083
|
-
> **Severity:** High
|
|
1084
|
-
|
|
1085
|
-
**UNSAFE:**
|
|
1086
|
-
|
|
1087
|
-
```ruby
|
|
1088
|
-
class Document < ApplicationRecord
|
|
1089
|
-
has_one_attached :file
|
|
1090
|
-
# No size limit — users can upload arbitrarily large files
|
|
1091
|
-
end
|
|
1092
|
-
```
|
|
1093
|
-
|
|
1094
|
-
**SAFE:**
|
|
1095
|
-
|
|
1096
|
-
```ruby
|
|
1097
|
-
class Document < ApplicationRecord
|
|
1098
|
-
has_one_attached :file
|
|
1099
|
-
|
|
1100
|
-
validates :file, size: { less_than: 10.megabytes }
|
|
1101
|
-
end
|
|
1102
|
-
```
|
|
1103
|
-
|
|
1104
|
-
### CHECK 4.3c: Safe file storage path
|
|
1105
|
-
|
|
1106
|
-
> **What to look for:** User-controlled filenames used directly in file paths; directory traversal via `../` in filenames
|
|
1107
|
-
> **Where to look:** `app/controllers/**/*.rb`, `lib/**/*.rb`
|
|
1108
|
-
> **Severity:** Critical
|
|
1109
|
-
|
|
1110
|
-
**UNSAFE:**
|
|
1111
|
-
|
|
1112
|
-
```ruby
|
|
1113
|
-
def download
|
|
1114
|
-
send_file Rails.root.join("uploads", params[:filename])
|
|
1115
|
-
# params[:filename] = "../../etc/passwd" allows path traversal
|
|
1116
|
-
end
|
|
1117
|
-
```
|
|
1118
|
-
|
|
1119
|
-
**SAFE:**
|
|
1120
|
-
|
|
1121
|
-
```ruby
|
|
1122
|
-
def download
|
|
1123
|
-
filename = File.basename(params[:filename])
|
|
1124
|
-
path = Rails.root.join("uploads", filename)
|
|
1125
|
-
if path.to_s.start_with?(Rails.root.join("uploads").to_s)
|
|
1126
|
-
send_file path
|
|
1127
|
-
else
|
|
1128
|
-
head :forbidden
|
|
1129
|
-
end
|
|
1130
|
-
end
|
|
1131
|
-
```
|
|
1132
|
-
|
|
1133
|
-
### CHECK 4.3d: No executable uploads in public directory
|
|
1134
|
-
|
|
1135
|
-
> **What to look for:** File uploads stored in `public/` directory where they could be served directly by the web server; executable file types not blocked
|
|
1136
|
-
> **Where to look:** `config/storage.yml`, `app/controllers/**/*.rb`
|
|
1137
|
-
> **Severity:** High
|
|
1138
|
-
|
|
1139
|
-
**UNSAFE:**
|
|
1140
|
-
|
|
1141
|
-
```ruby
|
|
1142
|
-
def upload
|
|
1143
|
-
path = Rails.root.join("public", "uploads", file.original_filename)
|
|
1144
|
-
File.open(path, "wb") { |f| f.write(file.read) }
|
|
1145
|
-
end
|
|
1146
|
-
```
|
|
1147
|
-
|
|
1148
|
-
**SAFE:**
|
|
1149
|
-
|
|
1150
|
-
```ruby
|
|
1151
|
-
# Use ActiveStorage — files stored outside public directory
|
|
1152
|
-
class Document < ApplicationRecord
|
|
1153
|
-
has_one_attached :file
|
|
1154
|
-
|
|
1155
|
-
validate :reject_dangerous_content_types
|
|
1156
|
-
|
|
1157
|
-
private
|
|
1158
|
-
def reject_dangerous_content_types
|
|
1159
|
-
dangerous = %w[text/html application/javascript application/x-httpd-php]
|
|
1160
|
-
if file.attached? && dangerous.include?(file.content_type)
|
|
1161
|
-
errors.add(:file, "type not allowed")
|
|
1162
|
-
end
|
|
1163
|
-
end
|
|
1164
|
-
end
|
|
1165
|
-
```
|
|
1166
|
-
|
|
1167
|
-
---
|
|
1168
|
-
|
|
1169
|
-
# Part 5: Infrastructure Security
|
|
1170
|
-
|
|
1171
|
-
These checks cover HTTP-level protections, API security, and dependency management.
|
|
1172
|
-
|
|
1173
|
-
## 5.1 HTTP Security Headers
|
|
1174
|
-
|
|
1175
|
-
### CHECK 5.1a: Force SSL in production
|
|
1176
|
-
|
|
1177
|
-
> **What to look for:** `force_ssl` not enabled in production; HSTS not configured
|
|
1178
|
-
> **Where to look:** `config/environments/production.rb`
|
|
1179
|
-
> **Severity:** Critical
|
|
1180
|
-
|
|
1181
|
-
**UNSAFE:**
|
|
1182
|
-
|
|
1183
|
-
```ruby
|
|
1184
|
-
# config/environments/production.rb
|
|
1185
|
-
# config.force_ssl = true (commented out or missing)
|
|
1186
|
-
```
|
|
1187
|
-
|
|
1188
|
-
**SAFE:**
|
|
1189
|
-
|
|
1190
|
-
```ruby
|
|
1191
|
-
# config/environments/production.rb
|
|
1192
|
-
config.force_ssl = true
|
|
1193
|
-
# Enables HSTS, redirects HTTP to HTTPS, marks cookies as secure
|
|
1194
|
-
```
|
|
1195
|
-
|
|
1196
|
-
### CHECK 5.1b: Content Security Policy configured
|
|
1197
|
-
|
|
1198
|
-
> **What to look for:** Missing Content-Security-Policy header; overly permissive CSP with `unsafe-inline` or `unsafe-eval`
|
|
1199
|
-
> **Where to look:** `config/initializers/content_security_policy.rb`
|
|
1200
|
-
> **Severity:** High
|
|
1201
|
-
|
|
1202
|
-
**UNSAFE:**
|
|
1203
|
-
|
|
1204
|
-
```ruby
|
|
1205
|
-
# No CSP configured, or:
|
|
1206
|
-
Rails.application.configure do
|
|
1207
|
-
config.content_security_policy do |policy|
|
|
1208
|
-
policy.script_src :unsafe_inline, :unsafe_eval, "*"
|
|
1209
|
-
end
|
|
1210
|
-
end
|
|
1211
|
-
```
|
|
1212
|
-
|
|
1213
|
-
**SAFE:**
|
|
1214
|
-
|
|
1215
|
-
```ruby
|
|
1216
|
-
Rails.application.configure do
|
|
1217
|
-
config.content_security_policy do |policy|
|
|
1218
|
-
policy.default_src :self
|
|
1219
|
-
policy.script_src :self
|
|
1220
|
-
policy.style_src :self, :unsafe_inline
|
|
1221
|
-
policy.img_src :self, :data, "https://storage.example.com"
|
|
1222
|
-
policy.connect_src :self
|
|
1223
|
-
end
|
|
1224
|
-
|
|
1225
|
-
config.content_security_policy_nonce_generator = ->(request) {
|
|
1226
|
-
request.session.id.to_s
|
|
1227
|
-
}
|
|
1228
|
-
end
|
|
1229
|
-
```
|
|
1230
|
-
|
|
1231
|
-
### CHECK 5.1c: Referrer-Policy and permissions headers
|
|
1232
|
-
|
|
1233
|
-
> **What to look for:** Missing `Referrer-Policy`, `Permissions-Policy`, or `X-Content-Type-Options` headers
|
|
1234
|
-
> **Where to look:** `config/initializers/**/*.rb`, `app/controllers/application_controller.rb`
|
|
1235
|
-
> **Severity:** Medium
|
|
1236
|
-
|
|
1237
|
-
**UNSAFE:**
|
|
1238
|
-
|
|
1239
|
-
```ruby
|
|
1240
|
-
# No additional security headers configured
|
|
1241
|
-
```
|
|
1242
|
-
|
|
1243
|
-
**SAFE:**
|
|
1244
|
-
|
|
1245
|
-
```ruby
|
|
1246
|
-
# config/initializers/default_headers.rb
|
|
1247
|
-
Rails.application.config.action_dispatch.default_headers.merge!(
|
|
1248
|
-
"Referrer-Policy" => "strict-origin-when-cross-origin",
|
|
1249
|
-
"Permissions-Policy" => "camera=(), microphone=(), geolocation=()",
|
|
1250
|
-
"X-Content-Type-Options" => "nosniff"
|
|
1251
|
-
)
|
|
1252
|
-
```
|
|
1253
|
-
|
|
1254
|
-
---
|
|
1255
|
-
|
|
1256
|
-
## 5.2 API Security
|
|
1257
|
-
|
|
1258
|
-
### CHECK 5.2a: API authentication on all endpoints
|
|
1259
|
-
|
|
1260
|
-
> **What to look for:** API controllers without authentication; endpoints that accept requests without valid tokens
|
|
1261
|
-
> **Where to look:** `app/controllers/api/**/*.rb`
|
|
1262
|
-
> **Severity:** Critical
|
|
1263
|
-
|
|
1264
|
-
**UNSAFE:**
|
|
1265
|
-
|
|
1266
|
-
```ruby
|
|
1267
|
-
class Api::V1::CardsController < ActionController::API
|
|
1268
|
-
def index
|
|
1269
|
-
@cards = Card.all
|
|
1270
|
-
end
|
|
1271
|
-
end
|
|
1272
|
-
```
|
|
1273
|
-
|
|
1274
|
-
**SAFE:**
|
|
1275
|
-
|
|
1276
|
-
```ruby
|
|
1277
|
-
class Api::V1::BaseController < ActionController::API
|
|
1278
|
-
before_action :authenticate_api_token
|
|
1279
|
-
|
|
1280
|
-
private
|
|
1281
|
-
def authenticate_api_token
|
|
1282
|
-
token = request.headers["Authorization"]&.remove("Bearer ")
|
|
1283
|
-
head :unauthorized unless token && ApiToken.active.exists?(token: token)
|
|
1284
|
-
end
|
|
1285
|
-
end
|
|
1286
|
-
|
|
1287
|
-
class Api::V1::CardsController < Api::V1::BaseController
|
|
1288
|
-
def index
|
|
1289
|
-
@cards = Current.account.cards.all
|
|
1290
|
-
end
|
|
1291
|
-
end
|
|
1292
|
-
```
|
|
1293
|
-
|
|
1294
|
-
### CHECK 5.2b: API response scoping
|
|
1295
|
-
|
|
1296
|
-
> **What to look for:** API responses that return data outside the authenticated user's tenant or permission scope
|
|
1297
|
-
> **Where to look:** `app/controllers/api/**/*.rb`, `app/serializers/**/*.rb`
|
|
1298
|
-
> **Severity:** High
|
|
1299
|
-
|
|
1300
|
-
**UNSAFE:**
|
|
1301
|
-
|
|
1302
|
-
```ruby
|
|
1303
|
-
def show
|
|
1304
|
-
@user = User.find(params[:id])
|
|
1305
|
-
render json: @user, include: [:sessions, :identity]
|
|
1306
|
-
end
|
|
1307
|
-
```
|
|
1308
|
-
|
|
1309
|
-
**SAFE:**
|
|
1310
|
-
|
|
1311
|
-
```ruby
|
|
1312
|
-
def show
|
|
1313
|
-
@user = Current.account.users.find(params[:id])
|
|
1314
|
-
render json: @user, only: [:id, :name, :email, :role]
|
|
1315
|
-
end
|
|
1316
|
-
```
|
|
1317
|
-
|
|
1318
|
-
### CHECK 5.2c: API rate limiting
|
|
1319
|
-
|
|
1320
|
-
> **What to look for:** API endpoints without rate limiting; missing throttling for expensive operations
|
|
1321
|
-
> **Where to look:** `app/controllers/api/**/*.rb`, `config/initializers/rack_attack.rb`
|
|
1322
|
-
> **Severity:** Medium
|
|
1323
|
-
|
|
1324
|
-
**UNSAFE:**
|
|
1325
|
-
|
|
1326
|
-
```ruby
|
|
1327
|
-
# No rate limiting configured
|
|
1328
|
-
class Api::V1::SearchController < Api::V1::BaseController
|
|
1329
|
-
def index
|
|
1330
|
-
@results = Card.search(params[:q])
|
|
1331
|
-
end
|
|
1332
|
-
end
|
|
1333
|
-
```
|
|
1334
|
-
|
|
1335
|
-
**SAFE:**
|
|
1336
|
-
|
|
1337
|
-
```ruby
|
|
1338
|
-
# config/initializers/rack_attack.rb
|
|
1339
|
-
Rack::Attack.throttle("api/requests", limit: 100, period: 1.minute) do |req|
|
|
1340
|
-
req.env["HTTP_AUTHORIZATION"]&.remove("Bearer ") if req.path.start_with?("/api/")
|
|
1341
|
-
end
|
|
1342
|
-
|
|
1343
|
-
# Or use Rails built-in rate limiting:
|
|
1344
|
-
class Api::V1::SearchController < Api::V1::BaseController
|
|
1345
|
-
rate_limit to: 30, within: 1.minute
|
|
1346
|
-
end
|
|
1347
|
-
```
|
|
1348
|
-
|
|
1349
|
-
---
|
|
1350
|
-
|
|
1351
|
-
## 5.3 Dependency Auditing
|
|
1352
|
-
|
|
1353
|
-
### CHECK 5.3a: No known vulnerable gems
|
|
1354
|
-
|
|
1355
|
-
> **What to look for:** Gems with known CVEs; outdated gems with security patches available
|
|
1356
|
-
> **Where to look:** `Gemfile`, `Gemfile.lock`
|
|
1357
|
-
> **Severity:** High
|
|
1358
|
-
|
|
1359
|
-
**Verification command:**
|
|
1360
|
-
|
|
1361
|
-
```bash
|
|
1362
|
-
bundle audit check --update
|
|
1363
|
-
```
|
|
1364
|
-
|
|
1365
|
-
**UNSAFE:**
|
|
1366
|
-
|
|
1367
|
-
```
|
|
1368
|
-
# bundle audit reports vulnerabilities
|
|
1369
|
-
# Gemfile.lock contains gems with known CVEs
|
|
1370
|
-
```
|
|
1371
|
-
|
|
1372
|
-
**SAFE:**
|
|
1373
|
-
|
|
1374
|
-
```
|
|
1375
|
-
# bundle audit reports: No vulnerabilities found
|
|
1376
|
-
# All gems patched to versions without known CVEs
|
|
1377
|
-
```
|
|
1378
|
-
|
|
1379
|
-
### CHECK 5.3b: JavaScript dependencies audited
|
|
1380
|
-
|
|
1381
|
-
> **What to look for:** npm/yarn packages with known vulnerabilities
|
|
1382
|
-
> **Where to look:** `package.json`, `yarn.lock`, `package-lock.json`
|
|
1383
|
-
> **Severity:** High
|
|
1384
|
-
|
|
1385
|
-
**Verification command:**
|
|
1386
|
-
|
|
1387
|
-
```bash
|
|
1388
|
-
yarn audit
|
|
1389
|
-
# Or: npm audit
|
|
1390
|
-
```
|
|
1391
|
-
|
|
1392
|
-
**UNSAFE:**
|
|
1393
|
-
|
|
1394
|
-
```
|
|
1395
|
-
# yarn audit reports critical or high severity vulnerabilities
|
|
1396
|
-
```
|
|
1397
|
-
|
|
1398
|
-
**SAFE:**
|
|
1399
|
-
|
|
1400
|
-
```
|
|
1401
|
-
# yarn audit reports 0 vulnerabilities
|
|
1402
|
-
# Or all remaining advisories have been reviewed and accepted
|
|
1403
|
-
```
|
|
1404
|
-
|
|
1405
|
-
---
|
|
1406
|
-
|
|
1407
|
-
# Part 6: Security Verification Checklist
|
|
1408
|
-
|
|
1409
|
-
## 6.1 Agent Check Protocol
|
|
1410
|
-
|
|
1411
|
-
When verifying security after a development phase, follow this protocol to map changed files to relevant checks.
|
|
1412
|
-
|
|
1413
|
-
### Step 1: Identify changed files
|
|
1414
|
-
|
|
1415
|
-
```bash
|
|
1416
|
-
git diff --name-only HEAD~1
|
|
1417
|
-
# Or for a phase: git diff <phase-start-sha>..HEAD --name-only
|
|
1418
|
-
```
|
|
1419
|
-
|
|
1420
|
-
### Step 2: Map files to check sections
|
|
1421
|
-
|
|
1422
|
-
| Changed file pattern | Applicable sections |
|
|
1423
|
-
|---|---|
|
|
1424
|
-
| `app/models/**/*.rb` | 1.1 (SQL), 2.2 (mass assignment), 3.2 (IDOR), 4.1 (secrets), 4.3 (uploads) |
|
|
1425
|
-
| `app/controllers/**/*.rb` | 1.1d (unscoped find), 1.2e (content-type), 2.1 (CSRF), 2.2 (strong params), 2.3 (redirects), 3.1 (auth), 3.2 (authz) |
|
|
1426
|
-
| `app/views/**/*.erb` | 1.2 (XSS), 2.1b (CSRF tokens) |
|
|
1427
|
-
| `app/controllers/api/**/*.rb` | 5.2 (API security) |
|
|
1428
|
-
| `config/routes.rb` | 2.1c (GET state changes), 2.3 (redirects) |
|
|
1429
|
-
| `config/environments/**/*.rb` | 4.1c (secret key), 5.1a (SSL), 5.1b (CSP) |
|
|
1430
|
-
| `config/initializers/**/*.rb` | 3.3b (cookies), 4.2a (param filtering), 5.1 (headers) |
|
|
1431
|
-
| `db/migrate/**/*.rb` | 3.1b (password storage) |
|
|
1432
|
-
| `lib/**/*.rb` | 1.1b (raw SQL), 1.3 (command injection) |
|
|
1433
|
-
| `Gemfile` | 5.3a (vulnerable gems) |
|
|
1434
|
-
| `package.json` / `yarn.lock` | 5.3b (JS dependencies) |
|
|
1435
|
-
| `app/javascript/**/*.js` | 1.2d (XSS), 2.1b (CSRF token in fetch) |
|
|
1436
|
-
|
|
1437
|
-
### Step 3: Run applicable checks
|
|
1438
|
-
|
|
1439
|
-
For each applicable section, scan the changed files using the "Where to look" glob pattern and "What to look for" pattern. Report findings as:
|
|
1440
|
-
|
|
1441
|
-
```
|
|
1442
|
-
PASS: CHECK 1.1a — No string interpolation in SQL (scanned 3 files)
|
|
1443
|
-
FAIL: CHECK 3.2a — Unscoped find in app/controllers/boards_controller.rb:15
|
|
1444
|
-
SKIP: CHECK 4.3a — No file upload changes in this phase
|
|
1445
|
-
```
|
|
1446
|
-
|
|
1447
|
-
### Step 4: Run automated tools
|
|
1448
|
-
|
|
1449
|
-
```bash
|
|
1450
|
-
# Dependency audit
|
|
1451
|
-
bundle audit check --update
|
|
1452
|
-
yarn audit
|
|
1453
|
-
|
|
1454
|
-
# Static analysis (if Brakeman is available)
|
|
1455
|
-
brakeman --no-pager -q
|
|
1456
|
-
|
|
1457
|
-
# Check for secrets in git history
|
|
1458
|
-
git log --diff-filter=A --name-only -- "*.key" "*.pem" ".env*"
|
|
1459
|
-
```
|
|
1460
|
-
|
|
1461
|
-
---
|
|
1462
|
-
|
|
1463
|
-
## 6.2 Quick-Reference Checklist
|
|
1464
|
-
|
|
1465
|
-
All checks from Parts 1-5 in a single table for quick scanning.
|
|
1466
|
-
|
|
1467
|
-
| Check | Name | Severity | Grep pattern | Files |
|
|
1468
|
-
|---|---|---|---|---|
|
|
1469
|
-
| 1.1a | No string interpolation in SQL | Critical | `\.where\(["'].*#\{` | `app/models/**/*.rb` |
|
|
1470
|
-
| 1.1b | Parameterized raw SQL | Critical | `\.execute\(["'].*#\{` | `app/models/**/*.rb`, `lib/**/*.rb` |
|
|
1471
|
-
| 1.1c | Safe column names in order | High | `\.order\(params` | `app/controllers/**/*.rb` |
|
|
1472
|
-
| 1.1d | Scoped find for user lookups | High | `\.find\(params\[` | `app/controllers/**/*.rb` |
|
|
1473
|
-
| 1.2a | No raw/html_safe on user input | Critical | `\.html_safe\|raw(` | `app/views/**/*.erb` |
|
|
1474
|
-
| 1.2b | Sanitized rich text | High | `\.to_s\.html_safe` | `app/views/**/*.erb` |
|
|
1475
|
-
| 1.2c | Safe link_to href values | High | `link_to.*params\[` | `app/views/**/*.erb` |
|
|
1476
|
-
| 1.2d | JSON escaped in script tags | High | `<script>.*to_json` | `app/views/**/*.erb` |
|
|
1477
|
-
| 1.2e | Content-Type for non-HTML | Medium | `render plain:` | `app/controllers/**/*.rb` |
|
|
1478
|
-
| 1.3a | No user input in shell calls | Critical | `system\(["'].*#\{` | `app/**/*.rb`, `lib/**/*.rb` |
|
|
1479
|
-
| 1.3b | Shellwords for shell use | High | `Shellwords\.escape` | `app/**/*.rb`, `lib/**/*.rb` |
|
|
1480
|
-
| 1.4a | \\A and \\z in regex | High | `format:.*\/\^` | `app/models/**/*.rb` |
|
|
1481
|
-
| 2.1a | CSRF protection enabled | Critical | `skip_before_action :verify_authenticity` | `app/controllers/**/*.rb` |
|
|
1482
|
-
| 2.1b | Authenticity token in forms | High | `<form[^>]*method` | `app/views/**/*.erb` |
|
|
1483
|
-
| 2.1c | Non-GET for state changes | High | `get.*destroy\|get.*delete\|get.*create` | `config/routes.rb` |
|
|
1484
|
-
| 2.2a | Strong parameters | Critical | `params\.permit!` | `app/controllers/**/*.rb` |
|
|
1485
|
-
| 2.2b | No admin attrs in permit | High | `permit.*:admin\|permit.*:role` | `app/controllers/**/*.rb` |
|
|
1486
|
-
| 2.2c | Nested attributes scoped | Medium | `accepts_nested_attributes_for` | `app/models/**/*.rb` |
|
|
1487
|
-
| 2.3a | No open redirects | High | `redirect_to params\[` | `app/controllers/**/*.rb` |
|
|
1488
|
-
| 2.3b | Redirect allowlists | Medium | `allow_other_host: true` | `app/controllers/**/*.rb` |
|
|
1489
|
-
| 3.1a | Auth on all controllers | Critical | `skip_before_action :authenticate` | `app/controllers/**/*.rb` |
|
|
1490
|
-
| 3.1b | Secure password handling | Critical | `Digest::MD5\|Digest::SHA1` | `app/models/**/*.rb` |
|
|
1491
|
-
| 3.1c | Timing-safe comparison | High | `==.*token\|==.*secret` | `app/controllers/**/*.rb` |
|
|
1492
|
-
| 3.1d | Rate limiting on auth | High | `rate_limit` | `app/controllers/sessions_controller.rb` |
|
|
1493
|
-
| 3.2a | Scoped resource lookups | Critical | `Model\.find\(params` | `app/controllers/**/*.rb` |
|
|
1494
|
-
| 3.2b | Authz on destructive actions | High | `def destroy` | `app/controllers/**/*.rb` |
|
|
1495
|
-
| 3.2c | Tenant isolation | Critical | `Current\.account` | `app/models/**/*.rb` |
|
|
1496
|
-
| 3.2d | Nested resource authz | High | `find\(params\[:id\]\)` | `app/controllers/**/*.rb` |
|
|
1497
|
-
| 3.3a | Session regeneration | High | `reset_session` | `app/controllers/sessions_controller.rb` |
|
|
1498
|
-
| 3.3b | Secure cookie config | High | `session_store.*secure` | `config/**/*.rb` |
|
|
1499
|
-
| 3.3c | Session expiration | Medium | `expire_after` | `config/**/*.rb` |
|
|
1500
|
-
| 4.1a | No hardcoded secrets | Critical | `API_KEY\|SECRET\|password.*=.*["']` | `app/**/*.rb`, `config/**/*.rb` |
|
|
1501
|
-
| 4.1b | Credentials encrypted | High | `credentials\.yml\.enc` | `config/**/*.yml` |
|
|
1502
|
-
| 4.1c | Secret key base configured | Critical | `secret_key_base` | `config/environments/production.rb` |
|
|
1503
|
-
| 4.2a | Sensitive params filtered | High | `filter_parameters` | `config/initializers/**/*.rb` |
|
|
1504
|
-
| 4.2b | No sensitive data in logs | High | `logger.*token\|logger.*password` | `app/**/*.rb` |
|
|
1505
|
-
| 4.3a | Content type validation | High | `content_type:` | `app/models/**/*.rb` |
|
|
1506
|
-
| 4.3b | File size limits | High | `size:.*less_than` | `app/models/**/*.rb` |
|
|
1507
|
-
| 4.3c | Safe file storage path | Critical | `send_file.*params` | `app/controllers/**/*.rb` |
|
|
1508
|
-
| 4.3d | No executable uploads | High | `public.*uploads` | `config/storage.yml` |
|
|
1509
|
-
| 5.1a | Force SSL in production | Critical | `force_ssl` | `config/environments/production.rb` |
|
|
1510
|
-
| 5.1b | CSP configured | High | `content_security_policy` | `config/initializers/**/*.rb` |
|
|
1511
|
-
| 5.1c | Security headers set | Medium | `Referrer-Policy\|Permissions-Policy` | `config/initializers/**/*.rb` |
|
|
1512
|
-
| 5.2a | API authentication | Critical | `before_action.*authenticate` | `app/controllers/api/**/*.rb` |
|
|
1513
|
-
| 5.2b | API response scoping | High | `Current\.account` | `app/controllers/api/**/*.rb` |
|
|
1514
|
-
| 5.2c | API rate limiting | Medium | `rate_limit\|Rack::Attack` | `app/controllers/api/**/*.rb` |
|
|
1515
|
-
| 5.3a | No vulnerable gems | High | `bundle audit` | `Gemfile.lock` |
|
|
1516
|
-
| 5.3b | JS dependencies audited | High | `yarn audit\|npm audit` | `package.json` |
|
|
1517
|
-
|
|
1518
|
-
---
|
|
1519
|
-
|
|
1520
|
-
**Document Version**: 1.0
|
|
1521
|
-
**Last Updated**: 2026-02-15
|
|
1522
|
-
**Maintainer**: Development Team
|