# 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.