From 114b43e6808a8cc963cd764a2fe3cb92847ff2c4 Mon Sep 17 00:00:00 2001 From: dl-alexandre <166029845+dl-alexandre@users.noreply.github.com> Date: Thu, 14 May 2026 23:56:02 -0700 Subject: [PATCH 1/4] Redact repo config from SASL progress reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The child spec built by `Ecto.Repo.Supervisor.wrap_child_spec/2` carries the adapter's start MFA in its `start:` field, and that MFA's argument list contains the full repo configuration — `:username`, `:password`, `:url`, `:hostname`, etc. When `:logger` is configured with `handle_otp_reports: true` and `handle_sasl_reports: true`, the supervisor's `progress` report inspects the child spec and prints the credentials in plaintext. Wrap the adapter MFA in an `Ecto.Repo.Supervisor.SensitiveData` struct that derives `Inspect` with no visible fields, then unwrap it in `start_child/4` before `apply/3`. The wrapper exists only while the args sit in the supervisor's child-spec registry — exactly the window during which SASL formats them. Adapter behavior is unchanged. Same shape as the fix `db_connection` applied for its own equivalent leak in elixir-ecto/db_connection#340. Closes #4718. --- lib/ecto/repo/supervisor.ex | 15 ++++++++++++++- test/ecto/repo/supervisor_test.exs | 27 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/ecto/repo/supervisor.ex b/lib/ecto/repo/supervisor.ex index bda7a38cc1..de161b6361 100644 --- a/lib/ecto/repo/supervisor.ex +++ b/lib/ecto/repo/supervisor.ex @@ -1,3 +1,9 @@ +defmodule Ecto.Repo.Supervisor.SensitiveData do + @moduledoc false + @derive {Inspect, only: []} + defstruct [:data] +end + defmodule Ecto.Repo.Supervisor do @moduledoc false use Supervisor @@ -209,6 +215,13 @@ defmodule Ecto.Repo.Supervisor do end end + # See #4718 and elixir-ecto/db_connection#340. The wrapper hides the + # adapter MFA — which contains the full repo config — from SASL progress + # reports, which inspect the child spec verbatim. + def start_child(%__MODULE__.SensitiveData{data: {mod, fun, args}}, name, adapter, meta) do + start_child({mod, fun, args}, name, adapter, meta) + end + def start_child({mod, fun, args}, name, adapter, meta) do case apply(mod, fun, args) do {:ok, pid} -> @@ -222,6 +235,6 @@ defmodule Ecto.Repo.Supervisor do end defp wrap_child_spec(%{start: start} = spec, args) do - %{spec | start: {__MODULE__, :start_child, [start | args]}} + %{spec | start: {__MODULE__, :start_child, [%__MODULE__.SensitiveData{data: start} | args]}} end end diff --git a/test/ecto/repo/supervisor_test.exs b/test/ecto/repo/supervisor_test.exs index 2a618ba33a..7f262ecabb 100644 --- a/test/ecto/repo/supervisor_test.exs +++ b/test/ecto/repo/supervisor_test.exs @@ -223,4 +223,31 @@ defmodule Ecto.Repo.SupervisorTest do end end end + + describe "child spec credential redaction (#4718)" do + test "the wrapper hides credentials from inspect/1" do + mfa = + {SomePool, :start_link, + [[username: "u", password: "s3cret-do-not-leak", url: "ecto://u:s3cret@h/d"]]} + + rendered = inspect(%Ecto.Repo.Supervisor.SensitiveData{data: mfa}) + + refute rendered =~ "s3cret" + refute rendered =~ "password" + refute rendered =~ "url" + assert rendered =~ "SensitiveData" + end + + test "start_child/4 unwraps the SensitiveData wrapper" do + # The wrapper clause must delegate to the same code path as the raw + # MFA clause. Verify with a no-op MFA that returns `:ignore`. + defmodule WrapperProbe do + def go, do: :ignore + end + + wrapped = %Ecto.Repo.Supervisor.SensitiveData{data: {WrapperProbe, :go, []}} + + assert Ecto.Repo.Supervisor.start_child(wrapped, :nope, :adapter, %{}) == :ignore + end + end end From b9cd376cf85b694f5b744170013bda9b88f8ceb8 Mon Sep 17 00:00:00 2001 From: dl-alexandre <166029845+dl-alexandre@users.noreply.github.com> Date: Fri, 15 May 2026 00:32:42 -0700 Subject: [PATCH 2/4] Improve SensitiveData placement and add @doc false - Define SensitiveData struct inside Ecto.Repo.Supervisor module instead of as a top-level sibling for better scoping and idiomatic Elixir structure. - Add @doc false to the internal start_child/4 clause that unwraps the wrapper. - These are follow-up improvements on top of the original PR. --- lib/ecto/repo/supervisor.ex | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/ecto/repo/supervisor.ex b/lib/ecto/repo/supervisor.ex index de161b6361..cb44c3f05c 100644 --- a/lib/ecto/repo/supervisor.ex +++ b/lib/ecto/repo/supervisor.ex @@ -1,10 +1,11 @@ -defmodule Ecto.Repo.Supervisor.SensitiveData do - @moduledoc false - @derive {Inspect, only: []} - defstruct [:data] -end defmodule Ecto.Repo.Supervisor do + defmodule SensitiveData do + @moduledoc false + @derive {Inspect, only: []} + defstruct [:data] + end + @moduledoc false use Supervisor require Logger @@ -215,6 +216,8 @@ defmodule Ecto.Repo.Supervisor do end end + @doc false + # See #4718 and elixir-ecto/db_connection#340. The wrapper hides the # adapter MFA — which contains the full repo config — from SASL progress # reports, which inspect the child spec verbatim. From b3241e8681d940b19c64d2a8ade3cc2249097cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 15 May 2026 11:18:18 +0200 Subject: [PATCH 3/4] Move SensitiveData module to its own definition --- lib/ecto/repo/supervisor.ex | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/ecto/repo/supervisor.ex b/lib/ecto/repo/supervisor.ex index cb44c3f05c..dd5552ceeb 100644 --- a/lib/ecto/repo/supervisor.ex +++ b/lib/ecto/repo/supervisor.ex @@ -1,11 +1,10 @@ +defmodule Ecto.Repo.SensitiveData do + @moduledoc false + @derive {Inspect, only: []} + defstruct [:data] +end defmodule Ecto.Repo.Supervisor do - defmodule SensitiveData do - @moduledoc false - @derive {Inspect, only: []} - defstruct [:data] - end - @moduledoc false use Supervisor require Logger @@ -216,12 +215,7 @@ defmodule Ecto.Repo.Supervisor do end end - @doc false - - # See #4718 and elixir-ecto/db_connection#340. The wrapper hides the - # adapter MFA — which contains the full repo config — from SASL progress - # reports, which inspect the child spec verbatim. - def start_child(%__MODULE__.SensitiveData{data: {mod, fun, args}}, name, adapter, meta) do + def start_child(%Ecto.Repo.SensitiveData{data: {mod, fun, args}}, name, adapter, meta) do start_child({mod, fun, args}, name, adapter, meta) end @@ -238,6 +232,6 @@ defmodule Ecto.Repo.Supervisor do end defp wrap_child_spec(%{start: start} = spec, args) do - %{spec | start: {__MODULE__, :start_child, [%__MODULE__.SensitiveData{data: start} | args]}} + %{spec | start: {__MODULE__, :start_child, [%Ecto.Repo.SensitiveData{data: start} | args]}} end end From c5a889e0b00f33012334ef695d4ec667f459684b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 15 May 2026 11:18:50 +0200 Subject: [PATCH 4/4] Apply suggestion from @josevalim --- test/ecto/repo/supervisor_test.exs | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/test/ecto/repo/supervisor_test.exs b/test/ecto/repo/supervisor_test.exs index 7f262ecabb..2a618ba33a 100644 --- a/test/ecto/repo/supervisor_test.exs +++ b/test/ecto/repo/supervisor_test.exs @@ -223,31 +223,4 @@ defmodule Ecto.Repo.SupervisorTest do end end end - - describe "child spec credential redaction (#4718)" do - test "the wrapper hides credentials from inspect/1" do - mfa = - {SomePool, :start_link, - [[username: "u", password: "s3cret-do-not-leak", url: "ecto://u:s3cret@h/d"]]} - - rendered = inspect(%Ecto.Repo.Supervisor.SensitiveData{data: mfa}) - - refute rendered =~ "s3cret" - refute rendered =~ "password" - refute rendered =~ "url" - assert rendered =~ "SensitiveData" - end - - test "start_child/4 unwraps the SensitiveData wrapper" do - # The wrapper clause must delegate to the same code path as the raw - # MFA clause. Verify with a no-op MFA that returns `:ignore`. - defmodule WrapperProbe do - def go, do: :ignore - end - - wrapped = %Ecto.Repo.Supervisor.SensitiveData{data: {WrapperProbe, :go, []}} - - assert Ecto.Repo.Supervisor.start_child(wrapped, :nope, :adapter, %{}) == :ignore - end - end end