Repository: phoenixframework/phoenix
Pull request #6267 (2025-05-27 12:22:59)

Scrub `payload["value"]` for LiveView form submit events
Its very possible we don't want to do this way, but I think sharing the implementation of a possible fix is valuable.

## The Problem

The `Last Message` on a GenServer crash can display the values from a form submit, which will not have Phoenix's parameter filtering logic applied to the `"value"` string. This was reported by a user of mine when submitting a form that takes a password.

![image](https://github.com/user-attachments/assets/110c5afd-fc03-485a-a5aa-1741bf6cf325)

## The solution

What I've done here is just a very naive match on this specific pattern of socket message to scrub values in the inspect protocol. The issue with this is that we're lifting a LV concern to a Phoenix concern potentially. Since all we are doing is scrubbing values that are configured for scrubbing, this seems safe enough to me to make a PR.

I'm happy if this is just a discussion starter and maybe we figure some other way out to do this 😄 


Patch:
diff --git a/guides/deployment/releases.md b/guides/deployment/releases.md
index ba3399872..84272ea41 100644
--- a/guides/deployment/releases.md
+++ b/guides/deployment/releases.md
@@ -140,7 +140,7 @@ And that's it! If you peek inside the `migrate` script, you'll see it wraps exac
 
 You can use the same approach used for migrations to create any custom command to run in production. The idea is that each command invokes `load_app`, which calls `Application.ensure_loaded/1` to load the current application without starting it.
 
-However, some commands may need to start the whole application. In such cases, `Application.ensure_all_started/1` must be used instead of `Application.load/1`. Keep in mind starting the application will all processes in its supervision tree, including the Phoenix endpoint. This can be circumvented by changing your supervision tree to not start certain children under certain conditions. For example, in the release commands file you could do:
+However, some commands may need to start the whole application. In such cases, `Application.ensure_all_started/1` must be used instead of `Application.load/1`. Keep in mind starting the application will start all processes in its supervision tree, including the Phoenix endpoint. This can be circumvented by changing your supervision tree to not start certain children under certain conditions. For example, in the release commands file you could do:
 
 ```elixir
 defp start_app do
diff --git a/guides/real_time/channels.md b/guides/real_time/channels.md
index eaca2fbd1..9163ee67c 100644
--- a/guides/real_time/channels.md
+++ b/guides/real_time/channels.md
@@ -384,7 +384,7 @@ That's all there is to our basic chat app. Fire up multiple browser tabs and you
 
 ## Using Token Authentication
 
-When we connect, we'll often need to authenticate the client. Fortunately, this is a 4-step process with [Phoenix.Token](https://hexdocs.pm/phoenix/Phoenix.Token.html).
+When we connect, we'll often need to authenticate the client. Fortunately, this is a 5-step process with [Phoenix.Token](https://hexdocs.pm/phoenix/Phoenix.Token.html).
 
 ### Step 1 - Enable the `auth_token` functionality in the socket
 
diff --git a/lib/phoenix/logger.ex b/lib/phoenix/logger.ex
index f46788736..db0bb06f9 100644
--- a/lib/phoenix/logger.ex
+++ b/lib/phoenix/logger.ex
@@ -172,10 +172,20 @@ defmodule Phoenix.Logger do
 
   defp discard_values(%{} = map, params) do
     Enum.into(map, %{}, fn {k, v} ->
-      if is_binary(k) and String.contains?(k, params) do
-        {k, "[FILTERED]"}
-      else
-        {k, discard_values(v, params)}
+      cond do
+        is_binary(k) and String.contains?(k, params) ->
+          {k, "[FILTERED]"}
+
+        is_binary(v) and String.contains?(v, params) ->
+          new_value =
+            Enum.reduce(params, v, fn param, v ->
+              Regex.replace(~r/#{Regex.escape(param)}=([^&]*)(&?)/, v, "#{param}=[FILTERED]\\2")
+            end)
+
+          {k, new_value}
+
+        true ->
+          {k, discard_values(v, params)}
       end
     end)
   end
diff --git a/lib/phoenix/socket/message.ex b/lib/phoenix/socket/message.ex
index 584d63e9d..b35f15ba7 100644
--- a/lib/phoenix/socket/message.ex
+++ b/lib/phoenix/socket/message.ex
@@ -35,6 +35,19 @@ defmodule Phoenix.Socket.Message do
         raise Phoenix.Socket.InvalidMessageError, "missing key #{inspect(err.key)}"
     end
   end
+
+  defimpl Inspect do
+    def inspect(%Phoenix.Socket.Message{} = msg, opts) do
+      processed_msg = process_message(msg)
+      Inspect.Any.inspect(processed_msg, opts)
+    end
+
+    defp process_message(%{payload: payload} = msg) when is_map(payload) do
+      %{msg | payload: Phoenix.Logger.filter_values(payload)}
+    end
+
+    defp process_message(msg), do: msg
+  end
 end
 
 defmodule Phoenix.Socket.Reply do


Test patch:
diff --git a/guides/testing/testing_contexts.md b/guides/testing/testing_contexts.md
index 71345b93e..7488f5c66 100644
--- a/guides/testing/testing_contexts.md
+++ b/guides/testing/testing_contexts.md
@@ -126,7 +126,7 @@ use Hello.DataCase, async: true
 
 There are some considerations you need to have in mind when running asynchronous tests with the sandbox, so please refer to the [`Ecto.Adapters.SQL.Sandbox`](https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html) for more information.
 
-Finally at the end of the of the `DataCase` module we can find a function named `errors_on` with some examples of how to use it. This function is used for testing any validation we may want to add to our schemas. Let's give it a try by adding our own validations and then testing them.
+Finally, at the end of the `DataCase` module we can find a function named `errors_on` with some examples of how to use it. This function is used for testing any validation we may want to add to our schemas. Let's give it a try by adding our own validations and then testing them.
 
 ## Testing schemas
 
diff --git a/guides/testing/testing_controllers.md b/guides/testing/testing_controllers.md
index 802e35c2d..3da1c33c7 100644
--- a/guides/testing/testing_controllers.md
+++ b/guides/testing/testing_controllers.md
@@ -105,7 +105,7 @@ end
 
 The first test starts with a `post/2` request. That's because once the form in the `/posts/new` page is submitted, it becomes a POST request to the create action. Because we have supplied valid attributes, the post should have been successfully created and we should have redirected to the show action of the new post. This new page will have an address like `/posts/ID`, where ID is the identifier of the post in the database.
 
-We then use `redirected_params(conn)` to get the ID of the post and then match that we indeed redirected to the show action. Finally, we do request a `get` request to the page we redirected to, allowing us to verify that the post was indeed created.
+We then use `redirected_params(conn)` to get the ID of the post and then match that we indeed redirected to the show action. Finally, we do a `get` request to the page we redirected to, allowing us to verify that the post was indeed created.
 
 For the second test, we simply test the failure scenario. If any invalid attribute is given, it should re-render the "New Post" page.
 
diff --git a/test/phoenix/socket/message_test.exs b/test/phoenix/socket/message_test.exs
new file mode 100644
index 000000000..ae5b820cc
--- /dev/null
+++ b/test/phoenix/socket/message_test.exs
@@ -0,0 +1,64 @@
+defmodule Phoenix.Socket.MessageTest do
+  use ExUnit.Case, async: true
+  doctest Phoenix.Socket.Message
+
+  alias Phoenix.Socket.Message
+
+  describe "inspect/2 custom implementation" do
+    test "filters sensitive values in form submit events" do
+      message = %Message{
+        topic: "lv:1",
+        event: "event",
+        payload: %{
+          "event" => "submit",
+          "type" => "form",
+          "value" => "username=john&password=secret123&email=john@example.com"
+        },
+        ref: "1",
+        join_ref: "1"
+      }
+
+      inspected = inspect(message)
+
+      assert inspected =~ "password=[FILTERED]"
+      assert inspected =~ "username=john"
+      assert inspected =~ "email=john@example.com"
+    end
+
+    test "filters sensitive values at the end of form submit events" do
+      message = %Message{
+        topic: "lv:1",
+        event: "event",
+        payload: %{
+          "event" => "submit",
+          "type" => "form",
+          "value" => "username=john&password=secret123"
+        },
+        ref: "1",
+        join_ref: "1"
+      }
+
+      inspected = inspect(message)
+
+      assert inspected =~ "username=john"
+      assert inspected =~ "password=[FILTERED]\""
+    end
+
+    test "handles malformed query strings gracefully" do
+      message = %Message{
+        topic: "lv:1",
+        event: "event",
+        payload: %{
+          "event" => "submit",
+          "type" => "form",
+          "value" => "invalid=query=string&password=secret"
+        },
+        ref: "1",
+        join_ref: "1"
+      }
+
+      inspected = inspect(message)
+      assert is_binary(inspected)
+    end
+  end
+end