From 90bf44ce90d09ee45905b862044de39236c0d784 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 25 Apr 2026 10:10:42 -0500 Subject: [PATCH] fix(submit): allowlist switch_tab values instead of trusting client input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `handle_event("switch_tab", ...)` ran `String.to_existing_atom/1` on the raw `tab` value from the browser, so a stale, forged, or mistyped client event with any unknown string crashed the LiveView with `ArgumentError: not an already existing atom`. Compares against an explicit `@valid_tabs` allowlist (`:single`, `:csv`, `:adif`) — known values switch the active tab, anything else is ignored. --- lib/microwaveprop_web/live/submit_live.ex | 14 ++++++++++-- .../live/submit_live_test.exs | 22 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/microwaveprop_web/live/submit_live.ex b/lib/microwaveprop_web/live/submit_live.ex index 66dfa1c1..367cc7d8 100644 --- a/lib/microwaveprop_web/live/submit_live.ex +++ b/lib/microwaveprop_web/live/submit_live.ex @@ -54,10 +54,20 @@ defmodule MicrowavepropWeb.SubmitLive do end end + @valid_tabs ~w(single csv adif)a + @impl true def handle_event("switch_tab", %{"tab" => tab}, socket) do - socket = assign(socket, active_tab: String.to_existing_atom(tab)) - {:noreply, MicrowavepropWeb.LiveStashGuard.stash(socket)} + case Enum.find(@valid_tabs, &(Atom.to_string(&1) == tab)) do + nil -> + # Stale or forged client event — ignore instead of crashing the + # LiveView via String.to_existing_atom/1. + {:noreply, socket} + + atom -> + socket = assign(socket, active_tab: atom) + {:noreply, MicrowavepropWeb.LiveStashGuard.stash(socket)} + end end def handle_event("validate", %{"contact" => contact_params}, socket) do diff --git a/test/microwaveprop_web/live/submit_live_test.exs b/test/microwaveprop_web/live/submit_live_test.exs index 1f524bf7..1aada6f4 100644 --- a/test/microwaveprop_web/live/submit_live_test.exs +++ b/test/microwaveprop_web/live/submit_live_test.exs @@ -43,6 +43,28 @@ defmodule MicrowavepropWeb.SubmitLiveTest do end end + describe "switch_tab" do + test "ignores an unknown tab string instead of crashing the LiveView", %{conn: conn} do + {:ok, lv, _html} = live(conn, ~p"/submit") + + # An unknown tab name used to flow into String.to_existing_atom/1 + # and bring the LiveView down. We expect the event to be a no-op + # and the process to stay alive. + render_hook(lv, "switch_tab", %{"tab" => "definitely-not-a-tab-#{System.unique_integer()}"}) + + assert Process.alive?(lv.pid) + end + + test "honors a known tab name", %{conn: conn} do + {:ok, lv, _html} = live(conn, ~p"/submit") + + html = render_hook(lv, "switch_tab", %{"tab" => "csv"}) + + # Active tab moved to CSV — the CSV section's heading is now visible. + assert html =~ "CSV" + end + end + describe "validate" do test "shows validation errors on change", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit")