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.
Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/ariadna.gemspec +0 -1
  3. data/data/agents/ariadna-codebase-mapper.md +34 -722
  4. data/data/agents/ariadna-debugger.md +44 -1139
  5. data/data/agents/ariadna-executor.md +75 -396
  6. data/data/agents/ariadna-planner.md +78 -1215
  7. data/data/agents/ariadna-roadmapper.md +55 -582
  8. data/data/agents/ariadna-verifier.md +60 -702
  9. data/data/ariadna/templates/config.json +8 -33
  10. data/data/ariadna/workflows/debug.md +28 -0
  11. data/data/ariadna/workflows/execute-phase.md +31 -513
  12. data/data/ariadna/workflows/map-codebase.md +20 -319
  13. data/data/ariadna/workflows/new-milestone.md +20 -365
  14. data/data/ariadna/workflows/new-project.md +19 -880
  15. data/data/ariadna/workflows/plan-phase.md +24 -443
  16. data/data/ariadna/workflows/progress.md +20 -376
  17. data/data/ariadna/workflows/quick.md +19 -221
  18. data/data/ariadna/workflows/roadmap-ops.md +28 -0
  19. data/data/ariadna/workflows/verify-work.md +23 -560
  20. data/data/commands/ariadna/add-phase.md +11 -22
  21. data/data/commands/ariadna/debug.md +11 -143
  22. data/data/commands/ariadna/execute-phase.md +12 -30
  23. data/data/commands/ariadna/insert-phase.md +7 -14
  24. data/data/commands/ariadna/map-codebase.md +16 -49
  25. data/data/commands/ariadna/new-milestone.md +12 -25
  26. data/data/commands/ariadna/new-project.md +22 -26
  27. data/data/commands/ariadna/plan-phase.md +13 -22
  28. data/data/commands/ariadna/progress.md +16 -6
  29. data/data/commands/ariadna/quick.md +9 -11
  30. data/data/commands/ariadna/remove-phase.md +9 -12
  31. data/data/commands/ariadna/verify-work.md +14 -19
  32. data/data/skills/rails-backend/API.md +138 -0
  33. data/data/skills/rails-backend/CONTROLLERS.md +154 -0
  34. data/data/skills/rails-backend/JOBS.md +132 -0
  35. data/data/skills/rails-backend/MODELS.md +213 -0
  36. data/data/skills/rails-backend/SKILL.md +169 -0
  37. data/data/skills/rails-frontend/ASSETS.md +154 -0
  38. data/data/skills/rails-frontend/COMPONENTS.md +253 -0
  39. data/data/skills/rails-frontend/SKILL.md +187 -0
  40. data/data/skills/rails-frontend/VIEWS.md +168 -0
  41. data/data/skills/rails-performance/PROFILING.md +106 -0
  42. data/data/skills/rails-performance/SKILL.md +217 -0
  43. data/data/skills/rails-security/AUDIT.md +118 -0
  44. data/data/skills/rails-security/SKILL.md +422 -0
  45. data/data/skills/rails-testing/FIXTURES.md +78 -0
  46. data/data/skills/rails-testing/SKILL.md +160 -0
  47. data/data/skills/rails-testing/SYSTEM-TESTS.md +73 -0
  48. data/lib/ariadna/installer.rb +11 -15
  49. data/lib/ariadna/tools/cli.rb +0 -12
  50. data/lib/ariadna/tools/config_manager.rb +10 -72
  51. data/lib/ariadna/tools/frontmatter.rb +23 -1
  52. data/lib/ariadna/tools/init.rb +201 -401
  53. data/lib/ariadna/tools/model_profiles.rb +6 -14
  54. data/lib/ariadna/tools/phase_manager.rb +1 -10
  55. data/lib/ariadna/tools/state_manager.rb +170 -451
  56. data/lib/ariadna/tools/template_filler.rb +4 -12
  57. data/lib/ariadna/tools/verification.rb +21 -399
  58. data/lib/ariadna/uninstaller.rb +9 -0
  59. data/lib/ariadna/version.rb +1 -1
  60. metadata +20 -91
  61. data/data/agents/ariadna-backend-executor.md +0 -261
  62. data/data/agents/ariadna-frontend-executor.md +0 -259
  63. data/data/agents/ariadna-integration-checker.md +0 -418
  64. data/data/agents/ariadna-phase-researcher.md +0 -469
  65. data/data/agents/ariadna-plan-checker.md +0 -622
  66. data/data/agents/ariadna-project-researcher.md +0 -618
  67. data/data/agents/ariadna-research-synthesizer.md +0 -236
  68. data/data/agents/ariadna-test-executor.md +0 -266
  69. data/data/ariadna/references/checkpoints.md +0 -772
  70. data/data/ariadna/references/continuation-format.md +0 -249
  71. data/data/ariadna/references/decimal-phase-calculation.md +0 -65
  72. data/data/ariadna/references/git-integration.md +0 -248
  73. data/data/ariadna/references/git-planning-commit.md +0 -38
  74. data/data/ariadna/references/model-profile-resolution.md +0 -32
  75. data/data/ariadna/references/model-profiles.md +0 -73
  76. data/data/ariadna/references/phase-argument-parsing.md +0 -61
  77. data/data/ariadna/references/planning-config.md +0 -194
  78. data/data/ariadna/references/questioning.md +0 -153
  79. data/data/ariadna/references/rails-conventions.md +0 -416
  80. data/data/ariadna/references/tdd.md +0 -267
  81. data/data/ariadna/references/ui-brand.md +0 -160
  82. data/data/ariadna/references/verification-patterns.md +0 -853
  83. data/data/ariadna/templates/codebase/architecture.md +0 -481
  84. data/data/ariadna/templates/codebase/concerns.md +0 -380
  85. data/data/ariadna/templates/codebase/conventions.md +0 -434
  86. data/data/ariadna/templates/codebase/integrations.md +0 -328
  87. data/data/ariadna/templates/codebase/stack.md +0 -189
  88. data/data/ariadna/templates/codebase/structure.md +0 -418
  89. data/data/ariadna/templates/codebase/testing.md +0 -606
  90. data/data/ariadna/templates/context.md +0 -283
  91. data/data/ariadna/templates/continue-here.md +0 -78
  92. data/data/ariadna/templates/debug-subagent-prompt.md +0 -91
  93. data/data/ariadna/templates/phase-prompt.md +0 -609
  94. data/data/ariadna/templates/planner-subagent-prompt.md +0 -117
  95. data/data/ariadna/templates/research-project/ARCHITECTURE.md +0 -439
  96. data/data/ariadna/templates/research-project/FEATURES.md +0 -168
  97. data/data/ariadna/templates/research-project/PITFALLS.md +0 -406
  98. data/data/ariadna/templates/research-project/STACK.md +0 -251
  99. data/data/ariadna/templates/research-project/SUMMARY.md +0 -247
  100. data/data/ariadna/templates/state.md +0 -176
  101. data/data/ariadna/templates/summary-complex.md +0 -59
  102. data/data/ariadna/templates/summary-minimal.md +0 -41
  103. data/data/ariadna/templates/summary-standard.md +0 -48
  104. data/data/ariadna/templates/user-setup.md +0 -310
  105. data/data/ariadna/workflows/add-phase.md +0 -111
  106. data/data/ariadna/workflows/add-todo.md +0 -157
  107. data/data/ariadna/workflows/audit-milestone.md +0 -241
  108. data/data/ariadna/workflows/check-todos.md +0 -176
  109. data/data/ariadna/workflows/complete-milestone.md +0 -644
  110. data/data/ariadna/workflows/diagnose-issues.md +0 -219
  111. data/data/ariadna/workflows/discovery-phase.md +0 -289
  112. data/data/ariadna/workflows/discuss-phase.md +0 -408
  113. data/data/ariadna/workflows/execute-plan.md +0 -448
  114. data/data/ariadna/workflows/help.md +0 -470
  115. data/data/ariadna/workflows/insert-phase.md +0 -129
  116. data/data/ariadna/workflows/list-phase-assumptions.md +0 -178
  117. data/data/ariadna/workflows/pause-work.md +0 -122
  118. data/data/ariadna/workflows/plan-milestone-gaps.md +0 -256
  119. data/data/ariadna/workflows/remove-phase.md +0 -154
  120. data/data/ariadna/workflows/research-phase.md +0 -74
  121. data/data/ariadna/workflows/resume-project.md +0 -306
  122. data/data/ariadna/workflows/set-profile.md +0 -80
  123. data/data/ariadna/workflows/settings.md +0 -145
  124. data/data/ariadna/workflows/transition.md +0 -493
  125. data/data/ariadna/workflows/update.md +0 -212
  126. data/data/ariadna/workflows/verify-phase.md +0 -226
  127. data/data/commands/ariadna/add-todo.md +0 -42
  128. data/data/commands/ariadna/audit-milestone.md +0 -42
  129. data/data/commands/ariadna/check-todos.md +0 -41
  130. data/data/commands/ariadna/complete-milestone.md +0 -136
  131. data/data/commands/ariadna/discuss-phase.md +0 -86
  132. data/data/commands/ariadna/help.md +0 -22
  133. data/data/commands/ariadna/list-phase-assumptions.md +0 -50
  134. data/data/commands/ariadna/pause-work.md +0 -35
  135. data/data/commands/ariadna/plan-milestone-gaps.md +0 -40
  136. data/data/commands/ariadna/reapply-patches.md +0 -110
  137. data/data/commands/ariadna/research-phase.md +0 -187
  138. data/data/commands/ariadna/resume-work.md +0 -40
  139. data/data/commands/ariadna/set-profile.md +0 -34
  140. data/data/commands/ariadna/settings.md +0 -36
  141. data/data/commands/ariadna/update.md +0 -37
  142. data/data/guides/backend.md +0 -3069
  143. data/data/guides/frontend.md +0 -1479
  144. data/data/guides/performance.md +0 -1193
  145. data/data/guides/security.md +0 -1522
  146. data/data/guides/style-guide.md +0 -1091
  147. data/data/guides/testing.md +0 -504
  148. data/data/templates.md +0 -94
@@ -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