- 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.
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.exlib/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.