Commit b4e217f
committed
Fix a stored XSS in user-supplied URLs, found by the CI gate nobody had run
The branch is 600 commits ahead of main with no pull request, so I checked which
workflows one would trigger. Six: rspec, rspec-system, audit-selftest,
ruby_lint, brakeman, factory-bot-lint. The design suite covers the first four
and I had been running them all session. I had never run the last two.
Brakeman failed -- LinkToHref, weak confidence, on
organizations/_details.html.erb -- and it was real. `Organization#url` validated
with `URI::DEFAULT_PARSER.make_regexp` and no scheme argument, which accepts any
scheme, so `javascript:alert(document.cookie)` was a valid organization URL. That
field is rendered with `link_to`, so an organization admin sets it and anyone who
views the page and clicks runs script in their own session. Weak confidence
describes the static analysis, not the exposure.
Restricting the scheme is not enough on its own, and that is the more
interesting half. `BroadcastAnnouncement#link` already restricted it to
`http https` and was still bypassable, because `format:` is unanchored and
matches a substring: "javascript:alert(1) http://decoy.example.com" satisfied
it. That field is the "More info" link on every user's dashboard -- the widest
exposure of the three, and it had looked fixed. Found by testing the fix rather
than trusting the pattern that was already there.
Two layers:
* HttpUrlValidatable -- anchored, http and https only -- on all three fields
that take a URL from a user: Organization#url, BroadcastAnnouncement#link,
AccountRequest#organization_website.
* essentials_external_link and essentials_safe_href at the four render sites.
Split in two because the dashboard link carries its own classes and
accessible name, so it needs the href checked rather than the anchor built
for it.
Both, because the validation guards one write path and a row can arrive by CSV
import, from the console, or from a database restored from before it existed. A
dangerous URL renders as plain text rather than nothing: dropping it hides the
problem from the only people who can fix it, and a bank looking at a nonsense
URL is how it gets corrected.
Checked before changing anything: 0 rows across all three fields held a
non-http(s) value, so nothing was invalidated. Had there been any, the data
migration would have had to come first.
43 new examples, and 21 of 36 watched failing against the old pattern.
Brakeman 1 warning -> 0, exit 0. factory_bot:lint exit 0. Full suite 3,430
examples 0 failures. All 29 audits the selector named: clean, with dead-code at
its documented 147.
The lesson is not about URLs. Four of six gates were covered; the two that were
not are where this was hiding, and it only surfaced because main bumped
brakeman 8.0.5 -> 8.0.6 in the merge and a new version brings new checks.
onboarding.md's post-merge routine has a step 9 now, naming both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent 6698b01 commit b4e217f
16 files changed
Lines changed: 302 additions & 10 deletions
File tree
- app
- helpers
- models
- concerns
- views
- admin/broadcast_announcements
- broadcast_announcements
- dashboard
- organizations
- docs
- spec
- helpers
- models/concerns
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
306 | 306 | | |
307 | 307 | | |
308 | 308 | | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
309 | 336 | | |
310 | 337 | | |
311 | 338 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
22 | 24 | | |
23 | 25 | | |
24 | | - | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
25 | 29 | | |
26 | 30 | | |
27 | 31 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
| 16 | + | |
15 | 17 | | |
16 | 18 | | |
17 | 19 | | |
18 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
19 | 23 | | |
20 | 24 | | |
21 | 25 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| 45 | + | |
| 46 | + | |
45 | 47 | | |
46 | 48 | | |
47 | 49 | | |
| |||
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
54 | | - | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
55 | 59 | | |
56 | 60 | | |
57 | 61 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
17 | 20 | | |
18 | 21 | | |
19 | 22 | | |
20 | 23 | | |
21 | | - | |
| 24 | + | |
22 | 25 | | |
23 | 26 | | |
24 | 27 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
30 | | - | |
31 | | - | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
471 | 471 | | |
472 | 472 | | |
473 | 473 | | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
474 | 498 | | |
475 | 499 | | |
476 | 500 | | |
| |||
0 commit comments