prop/bugs.md
Graham McIntire a3b97f9e98
fix(security): mass-assignment on owner edit + 3 minor hardening fixes
- Radio.normalize_proposed/1 now Map.take/2 the allowed-edit keys
  before normalization. Owner / admin / pending-edit submit paths all
  funnel through this boundary, so a crafted form can no longer
  mass-assign user_id, flagged_invalid, inserted_at, etc. on a
  contact via String.to_existing_atom -> Ecto.Changeset.change.
- Accounts.revoke_api_token/2 rescues Ecto.Query.CastError and
  returns {:error, :not_found} so a malformed UUID in
  DELETE /api/v1/me/api-tokens/:id renders the API's clean 404
  problem+json instead of a 500.
- ProfilesFile.read_etf decodes with :erlang.binary_to_term(bin, [:safe]).
  Defense-in-depth against tampered on-disk profiles (atom-table
  exhaustion via untrusted ETF).
- Regression tests: contact_edit_test.exs covers the rejected
  mass-assignment fields; accounts_api_token_test.exs covers the
  malformed-UUID path.
2026-05-11 19:04:16 -05:00

4 KiB

Bugs Found 2026-05-11

The first batch (7 items: pending-beacon access control, per_page contract, rate-limiter ETS lifecycle, unbounded grid endpoints, NEXRAD PNG quadratic unfilter, LiveTableFooter parser, ScoresFile parser) was fixed and removed from this file as each was resolved. The items below are additional bugs surfaced by a follow-up audit.

A. String.to_integer on user-controlled band param crashes LiveViews

Status: Fixed. Severity: High Category: Availability / unhandled crash on user input Files:

  • lib/microwaveprop_web/live/path_live.ex (formerly lines 227, 284)
  • lib/microwaveprop_web/live/map_live.ex (formerly line 198)

PathLive.handle_event("calculate", …) and the auto-calculate path both called String.to_integer(params["band"]) directly on the band form parameter. MapLive.handle_event("select_band", %{"value" => band}, …) did the same. A non-numeric band value — supplied via a hand-crafted URL (/path?band=abc) or a misbehaving JS hook — raised ArgumentError inside the LiveView process and crashed the channel.

Fix: Use the existing LiveHelpers.parse_int/2 (PathLive) and the existing parse_band_param/1 plus a small normalize_band_event/1 helper (MapLive). Unknown bands now fall back to a safe default or a no-op instead of crashing the process.

B. Mass assignment on Radio.apply_owner_edit/3

Status: Fixed. Severity: High Category: OWASP A04 Insecure Design / mass assignment Files:

  • lib/microwaveprop/radio.ex
  • lib/microwaveprop_web/live/contact_live/show.ex

apply_owner_edit and apply_admin_edit accept the LiveView form's params map, run it through normalize_proposed/1, then diff_against_contact/2, then apply_edit_to_contact/2. The final stage calls String.to_existing_atom(key) for every key in the diff and pipes the resulting {atom, value} pairs into Ecto.Changeset.change(contact, …).

normalize_proposed/1 did not whitelist keys, so an owner could craft a form POST with arbitrary fields — user_id, flagged_invalid, inserted_at, etc. — and have them persisted onto their own contact.

Fix: Tighten Radio.normalize_proposed/1 to Map.take/2 only the known editable keys (station1/2, grid1/2, band, mode, qso_timestamp, height1/2_ft, private) before any normalization / diffing. All three call sites (owner edit, admin edit, pending edit) share this boundary, so no other change was needed. Regression test in contact_edit_test.exs.

C. Accounts.revoke_api_token/2 raised Ecto.Query.CastError on a malformed UUID

Status: Fixed. Severity: Low Category: Availability / API contract

Repo.get_by(UserApiToken, id: token_id, …) raises Ecto.Query.CastError when token_id does not parse as a UUID. Phoenix.Ecto's Plug.Exception impl converts this to a 400 in the browser pipeline, but the API path (DELETE /api/v1/me/api-tokens/:id) bypasses the Plug.Exception fallback for actions that already return tagged tuples, so the malformed-UUID path leaked a 500 with a generic error body instead of the API's structured :not_found problem+json.

Fix: Rescue Ecto.Query.CastError in revoke_api_token/2 and return {:error, :not_found} — same shape that propagates as a clean 404 through the existing API fallback. Regression test in accounts_api_token_test.exs.

D. :erlang.binary_to_term/1 on disk-loaded propagation profiles

Status: Fixed (defense-in-depth — pre-existing low risk). Severity: Low Category: OWASP A08 Software Integrity Failures File: lib/microwaveprop/propagation/profiles_file.ex

read_etf/1 decoded .etf files from /data/profiles/... with :erlang.binary_to_term(binary) (no :safe option). Files are written by the propagation pipeline, so the realistic threat is filesystem tampering — by which point the box is already compromised. Still, the unsafe variant allows atom-table exhaustion DoS.

Fix: Pass [:safe] so unknown atoms and external function references are rejected on decode.