prop/bugs.md
Graham McIntire 0704253af8
fix(security,test): viewer-aware profile queries + create_contact ordering + valkey test isolation
- /u/:callsign profile no longer leaks private contacts or pending beacons;
  Radio.list_contacts_for_user/2 and Beacons.list_beacons_for_user/2 now
  filter by viewer (owner/admin see everything, others see only public).
- Radio.create_contact/2 places user_id on the struct before
  submission_changeset so validate_user_or_email/1 accepts authenticated
  submissions that omit submitter_email.
- valkey_test.exs runs async: false; its setup mutates global Application
  env and Process registry, which otherwise crashed concurrent ConnCase
  GridCache.clear/0 calls (Mox.UnexpectedCallError on SCAN).
2026-05-12 08:49:08 -05:00

118 lines
4.9 KiB
Markdown

# Bugs Found 2026-05-12
All three fixed; `mix test --seed 949374` now passes (3893 tests, 0 failures).
## 1. Public profile leaks private contacts and pending beacons [FIXED]
**Severity:** High
**Category:** Privacy / authorization bypass
**Files:**
- `lib/microwaveprop_web/live/user_profile_live.ex:21`
- `lib/microwaveprop_web/live/user_profile_live.ex:22`
- `lib/microwaveprop_web/live/user_profile_live.ex:92`
- `lib/microwaveprop_web/live/user_profile_live.ex:130`
- `lib/microwaveprop/beacons.ex:61`
`/u/:callsign` is mounted in the public LiveView session, but it loads
`Radio.list_contacts_for_user(user)` and `Beacons.list_beacons_for_user(user)`
without checking whether the visitor is the profile owner or an admin.
`list_contacts_for_user/1` returns every submitted contact for that user,
including contacts where `private == true`. The profile template then renders
those rows and links to `/contacts/:id`. The contact detail page correctly
404s for unauthorized viewers, but the public profile has already exposed the
contact timestamp, stations, band, mode, and distance.
`list_beacons_for_user/1` explicitly returns approved and pending beacons for
the public profile page. The profile template renders pending beacon callsign,
frequency, grid, keying, status, and link. This contradicts the beacon visibility
model where pending beacons should only be visible to the submitter and admins.
Suggested fix: split the profile query by viewer. Anonymous/non-owner visitors
should see only non-private contacts and approved beacons. Owners/admins can see
their private contacts and pending beacons. Add LiveView tests for anonymous,
owner, and admin views.
## 2. `Radio.create_contact/2` rejects authenticated submissions unless callers also pass an email [FIXED]
**Severity:** Medium
**Category:** Broken context contract / validation ordering
**Files:**
- `lib/microwaveprop/radio.ex:629`
- `lib/microwaveprop/radio.ex:632`
- `lib/microwaveprop/radio.ex:633`
- `lib/microwaveprop/radio/contact.ex:109`
- `lib/microwaveprop/radio/contact.ex:173`
`Radio.create_contact(attrs, user_id)` adds `user_id` after
`Contact.submission_changeset/2` has already run `validate_user_or_email/1`.
That validation only sees fields cast from `attrs`, so a valid authenticated
create with `user_id` but no `submitter_email` returns an invalid changeset with
`submitter_email: can't be blank`.
Confirmed with:
```text
Radio.create_contact(valid_attrs_without_submitter_email, user.id)
#=> {:error, %Ecto.Changeset{errors: [submitter_email: {"can't be blank", []}]}}
```
Current web/API call sites mask this by inserting the user's email into params
before calling the context, but the public function's contract and implementation
disagree. Any future trusted caller that relies on the `user_id` argument alone
will fail unexpectedly.
Suggested fix: put `user_id` into the struct or attrs before calling
`Contact.submission_changeset/2`, or move the ownership validation to after
`maybe_put_user_id/2`. Add a context-level regression test that passes
`user_id` without `submitter_email`.
## 3. Full test suite is order-dependent around Valkey-backed `GridCache.clear/0` [FIXED]
**Severity:** Medium
**Category:** Test isolation / flaky suite
**Files:**
- `test/support/conn_case.ex:45`
- `test/microwaveprop/weather/grid_cache_valkey_test.exs:26`
- `test/microwaveprop/weather/grid_cache_valkey_test.exs:35`
- `lib/microwaveprop/weather/grid_cache.ex:477`
One `mix test` run failed with 3 setup failures. `ConnCase` calls
`GridCache.clear/0` for every web test. If a Valkey cache test has registered
`Microwaveprop.Valkey.Conn` and swapped `:valkey_adapter`, `GridCache.clear/0`
takes the Valkey path and calls `Valkey.scan_match/1`. In the failed run, that
reached `Microwaveprop.Valkey.MockAdapter.command/3` without a `SCAN`
expectation, crashing unrelated web tests during setup.
Failure shape:
```text
Mox.UnexpectedCallError: no expectation defined for
Microwaveprop.Valkey.MockAdapter.command/3
args: [Microwaveprop.Valkey.Conn, ["SCAN", "0", "MATCH", "prop:wg:*", "COUNT", "500"], ...]
```
Verification:
```text
mix test --seed 949374
# 3880 tests, 3 failures, 6 skipped
mix precommit
# 3880 tests, 0 failures, 6 skipped
```
The subsequent passing `mix precommit` run makes this look order-dependent
rather than a deterministic failure.
Suggested fix: make Valkey test setup restore global process/env state before
other tests can observe it, or avoid global `Process.register/2` for `Conn`.
Also consider making `ConnCase` force the ETS fallback or installing a default
Valkey mock stub for `GridCache.clear/0`.
## Verification Notes
- `mix compile --warnings-as-errors` passed.
- `mix credo --strict` reported design/refactoring/style issues only; no bug-class findings were added from Credo.
- `mix test --seed 949374` failed as described in bug 3.
- `mix precommit` passed afterward, confirming the checked-in code still clears the normal project gate for this run.