From 50cdc6ddd57041cfac14c790e86d85a6bb458bc1 Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Tue, 6 Aug 2024 23:31:32 +0300 Subject: [PATCH 01/17] Implement optionnal CSRF check --- lib/phoenix/socket/transport.ex | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex index a1c5fd5467..2c924cdc5d 100644 --- a/lib/phoenix/socket/transport.ex +++ b/lib/phoenix/socket/transport.ex @@ -489,13 +489,10 @@ defmodule Phoenix.Socket.Transport do defp connect_session(conn, endpoint, {key, store, {csrf_token_key, init}}) do conn = Plug.Conn.fetch_cookies(conn) - with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"], - cookie when is_binary(cookie) <- conn.cookies[key], + with cookie when is_binary(cookie) <- conn.cookies[key], conn = put_in(conn.secret_key_base, endpoint.config(:secret_key_base)), {_, session} <- store.get(conn, cookie, init), - csrf_state when is_binary(csrf_state) <- - Plug.CSRFProtection.dump_state_from_session(session[csrf_token_key]), - true <- Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token) do + true <- maybe_check_csrf(conn, endpoint, session, csrf_token_key) do session else _ -> nil @@ -542,6 +539,18 @@ defmodule Phoenix.Socket.Transport do end end + defp maybe_check_csrf(conn, endpoint, session, csrf_token_key) do + if endpoint.config(:socket_check_csrf, true) do + with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"], + csrf_state when is_binary(csrf_state) <- + Plug.CSRFProtection.dump_state_from_session(session[csrf_token_key]) do + Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token) + end + else + true + end + end + defp check_origin_config(handler, endpoint, opts) do Phoenix.Config.cache(endpoint, {:check_origin, handler}, fn _ -> check_origin = From 492e4895f949fdb20c7181df808b5c6132af118d Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:05:30 +0300 Subject: [PATCH 02/17] Implement the `check_csrf` option --- lib/phoenix/socket/transport.ex | 30 ++++++++++++++--------------- lib/phoenix/transports/long_poll.ex | 6 +++++- lib/phoenix/transports/websocket.ex | 6 +++++- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex index 2c924cdc5d..506293e307 100644 --- a/lib/phoenix/socket/transport.ex +++ b/lib/phoenix/socket/transport.ex @@ -458,8 +458,11 @@ defmodule Phoenix.Socket.Transport do * `:user_agent` - the value of the "user-agent" request header + The CSRF check can be disabled with the option `:check_csrf`. Beware: if + the `origin` header check is disabled as well, then your app is vulnerable + to Cross-Site WebSocket Hijacking (CSWSH) attacks. """ - def connect_info(conn, endpoint, keys) do + def connect_info(conn, endpoint, keys, opts \\ [check_csrf: true]) do for key <- keys, into: %{} do case key do :peer_data -> @@ -478,7 +481,7 @@ defmodule Phoenix.Socket.Transport do {:user_agent, fetch_user_agent(conn)} {:session, session} -> - {:session, connect_session(conn, endpoint, session)} + {:session, connect_session(conn, endpoint, session, opts)} {key, val} -> {key, val} @@ -486,23 +489,24 @@ defmodule Phoenix.Socket.Transport do end end - defp connect_session(conn, endpoint, {key, store, {csrf_token_key, init}}) do + defp connect_session(conn, endpoint, {key, store, {csrf_token_key, init}}, opts) do conn = Plug.Conn.fetch_cookies(conn) + check_csrf = Keyword.get(opts, :check_csrf, true) with cookie when is_binary(cookie) <- conn.cookies[key], conn = put_in(conn.secret_key_base, endpoint.config(:secret_key_base)), {_, session} <- store.get(conn, cookie, init), - true <- maybe_check_csrf(conn, endpoint, session, csrf_token_key) do + true <- not check_csrf || csrf_token_valid?(conn, session, csrf_token_key) do session else _ -> nil end end - defp connect_session(conn, endpoint, {:mfa, {module, function, args}}) do + defp connect_session(conn, endpoint, {:mfa, {module, function, args}}, opts) do case apply(module, function, args) do session_config when is_list(session_config) -> - connect_session(conn, endpoint, init_session(session_config)) + connect_session(conn, endpoint, init_session(session_config), opts) other -> raise ArgumentError, @@ -539,15 +543,11 @@ defmodule Phoenix.Socket.Transport do end end - defp maybe_check_csrf(conn, endpoint, session, csrf_token_key) do - if endpoint.config(:socket_check_csrf, true) do - with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"], - csrf_state when is_binary(csrf_state) <- - Plug.CSRFProtection.dump_state_from_session(session[csrf_token_key]) do - Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token) - end - else - true + defp csrf_token_valid?(conn, session, csrf_token_key) do + with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"], + csrf_state when is_binary(csrf_state) <- + Plug.CSRFProtection.dump_state_from_session(session[csrf_token_key]) do + Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token) end end diff --git a/lib/phoenix/transports/long_poll.ex b/lib/phoenix/transports/long_poll.ex index de3365cee9..37da2fcda0 100644 --- a/lib/phoenix/transports/long_poll.ex +++ b/lib/phoenix/transports/long_poll.ex @@ -4,6 +4,7 @@ defmodule Phoenix.Transports.LongPoll do # 10MB @max_base64_size 10_000_000 + @connect_info_opts [:check_csrf] import Plug.Conn alias Phoenix.Socket.{V1, V2, Transport} @@ -136,7 +137,10 @@ defmodule Phoenix.Transports.LongPoll do (System.system_time(:millisecond) |> Integer.to_string()) keys = Keyword.get(opts, :connect_info, []) - connect_info = Transport.connect_info(conn, endpoint, keys) + + connect_info = + Transport.connect_info(conn, endpoint, keys, Keyword.take(opts, @connect_info_opts)) + arg = {endpoint, handler, opts, conn.params, priv_topic, connect_info} spec = {Phoenix.Transports.LongPoll.Server, arg} diff --git a/lib/phoenix/transports/websocket.ex b/lib/phoenix/transports/websocket.ex index f04cc4350d..dfc7bd2508 100644 --- a/lib/phoenix/transports/websocket.ex +++ b/lib/phoenix/transports/websocket.ex @@ -15,6 +15,8 @@ defmodule Phoenix.Transports.WebSocket do # @behaviour Plug + @connect_info_opts [:check_csrf] + import Plug.Conn alias Phoenix.Socket.{V1, V2, Transport} @@ -45,7 +47,9 @@ defmodule Phoenix.Transports.WebSocket do %{params: params} = conn -> keys = Keyword.get(opts, :connect_info, []) - connect_info = Transport.connect_info(conn, endpoint, keys) + + connect_info = + Transport.connect_info(conn, endpoint, keys, Keyword.take(opts, @connect_info_opts)) config = %{ endpoint: endpoint, From e845ac87be5e20e44d921e24ba34d595a9d60179 Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:05:55 +0300 Subject: [PATCH 03/17] Document `check_csrf` option in `Phoenix.Endpoint` --- lib/phoenix/endpoint.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex index 465420ac29..d81b8b63ea 100644 --- a/lib/phoenix/endpoint.ex +++ b/lib/phoenix/endpoint.ex @@ -859,6 +859,10 @@ defmodule Phoenix.Endpoint do The MFA is invoked with the request `%URI{}` as the first argument, followed by arguments in the MFA list, and must return a boolean. + * `:check_csrf` - if the transport should perform CSRF check. If `origin` check is disabled as + well as CSRF check, your app is vulnerable to Cross-Site WebSocket Hijacking (CSWSH) attacks. + Defaults to `true` + * `:code_reloader` - enable or disable the code reloader. Defaults to your endpoint configuration From 759eed4f3503d222bfe75120e2c0d7d840ab982f Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:08:55 +0300 Subject: [PATCH 04/17] Add test for `check_csrf` option --- test/phoenix/socket/transport_test.exs | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/test/phoenix/socket/transport_test.exs b/test/phoenix/socket/transport_test.exs index 2a3e3df9f7..05700342dd 100644 --- a/test/phoenix/socket/transport_test.exs +++ b/test/phoenix/socket/transport_test.exs @@ -276,7 +276,7 @@ defmodule Phoenix.Socket.TransportTest do end end - describe "connect_info/3" do + describe "connect_info/4" do defp load_connect_info(connect_info) do [connect_info: connect_info] = Transport.load_config(connect_info: connect_info) connect_info @@ -330,5 +330,31 @@ defmodule Phoenix.Socket.TransportTest do |> Transport.connect_info(Endpoint, connect_info) end + test "loads the session when CSRF is disabled despite CSRF token not being provided" do + conn = conn(:get, "https://foo.com/") |> Endpoint.call([]) + session_cookie = conn.cookies["_hello_key"] + + connect_info = load_connect_info(session: {Endpoint, :session_config, []}) + + assert %{session: %{"from_session" => "123"}} = + conn(:get, "https://foo.com/") + |> put_req_cookie("_hello_key", session_cookie) + |> fetch_query_params() + |> Transport.connect_info(Endpoint, connect_info, check_csrf: false) + end + + test "doesn't load session when an invalid CSRF token is provided" do + conn = conn(:get, "https://foo.com/") |> Endpoint.call([]) + invalid_csrf_token = "some invalid CSRF token" + session_cookie = conn.cookies["_hello_key"] + + connect_info = load_connect_info(session: {Endpoint, :session_config, []}) + + assert %{session: nil} = + conn(:get, "https://foo.com/", _csrf_token: invalid_csrf_token) + |> put_req_cookie("_hello_key", session_cookie) + |> fetch_query_params() + |> Transport.connect_info(Endpoint, connect_info) + end end end From 467e3786af24b43641f94380bd14042c41ebc070 Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Sun, 13 Oct 2024 19:27:22 +0300 Subject: [PATCH 05/17] Use boolean operator in condition --- lib/phoenix/socket/transport.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex index 506293e307..1af9e73ab8 100644 --- a/lib/phoenix/socket/transport.ex +++ b/lib/phoenix/socket/transport.ex @@ -496,7 +496,7 @@ defmodule Phoenix.Socket.Transport do with cookie when is_binary(cookie) <- conn.cookies[key], conn = put_in(conn.secret_key_base, endpoint.config(:secret_key_base)), {_, session} <- store.get(conn, cookie, init), - true <- not check_csrf || csrf_token_valid?(conn, session, csrf_token_key) do + true <- not check_csrf or csrf_token_valid?(conn, session, csrf_token_key) do session else _ -> nil From 5f2bf63ffed9af6dc2093975200f2b54902335a8 Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:56:58 +0300 Subject: [PATCH 06/17] Add test that checks disabling both origin and CSRF checks raises --- test/phoenix/socket/transport_test.exs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/phoenix/socket/transport_test.exs b/test/phoenix/socket/transport_test.exs index 05700342dd..87caeea22a 100644 --- a/test/phoenix/socket/transport_test.exs +++ b/test/phoenix/socket/transport_test.exs @@ -237,6 +237,13 @@ defmodule Phoenix.Socket.TransportTest do # an allowed host refute check_origin("https://host.com/", check_origin: mfa).halted end + + test "raises if both :check_origin and :check_csrf are set to false" do + assert_raise ArgumentError, ~r/One of :check_origin and :check_csrf must be set/, fn -> + check_origin("https://host.com/", check_origin: false, check_csrf: false) + end + end + end ## Check subprotocols From d80a5352dda060768f4335edb6debbc30b45b20f Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:57:34 +0300 Subject: [PATCH 07/17] Add check that one of origin and CSRF check is performed --- lib/phoenix/socket/transport.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex index 1af9e73ab8..fcc3f141fc 100644 --- a/lib/phoenix/socket/transport.ex +++ b/lib/phoenix/socket/transport.ex @@ -332,8 +332,13 @@ defmodule Phoenix.Socket.Transport do import Plug.Conn origin = conn |> get_req_header("origin") |> List.first() check_origin = check_origin_config(handler, endpoint, opts) + check_csrf = opts[:check_csrf] cond do + check_origin == false and check_csrf == false -> + raise ArgumentError, + "One of :check_origin and :check_csrf must be set" + is_nil(origin) or check_origin == false -> conn From f5b45bf31a6b3c370acd9faf6f9218b989a1e706 Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Thu, 24 Oct 2024 22:43:17 +0300 Subject: [PATCH 08/17] Update Phoenix.Socket.Transport.connect_info/4 doc We take into account that it's no longer possible to have both CSRF and origin checks disabled at the same time --- lib/phoenix/socket/transport.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex index fcc3f141fc..a425f56131 100644 --- a/lib/phoenix/socket/transport.ex +++ b/lib/phoenix/socket/transport.ex @@ -463,9 +463,7 @@ defmodule Phoenix.Socket.Transport do * `:user_agent` - the value of the "user-agent" request header - The CSRF check can be disabled with the option `:check_csrf`. Beware: if - the `origin` header check is disabled as well, then your app is vulnerable - to Cross-Site WebSocket Hijacking (CSWSH) attacks. + The CSRF check can be disabled with the `:check_csrf` option. """ def connect_info(conn, endpoint, keys, opts \\ [check_csrf: true]) do for key <- keys, into: %{} do From f9a8f2944abe8d04d843071521fd991cc5ddc233 Mon Sep 17 00:00:00 2001 From: Tangui <29804907+tanguilp@users.noreply.github.com> Date: Thu, 24 Oct 2024 22:45:31 +0300 Subject: [PATCH 09/17] Set the connect_option default options to `[]` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/phoenix/socket/transport.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex index a425f56131..82f1bbb382 100644 --- a/lib/phoenix/socket/transport.ex +++ b/lib/phoenix/socket/transport.ex @@ -465,7 +465,7 @@ defmodule Phoenix.Socket.Transport do The CSRF check can be disabled with the `:check_csrf` option. """ - def connect_info(conn, endpoint, keys, opts \\ [check_csrf: true]) do + def connect_info(conn, endpoint, keys, opts \\ []) do for key <- keys, into: %{} do case key do :peer_data -> From 0f3fdab81211fe7aba684d9bdf9e0e64e5d5c7db Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Thu, 24 Oct 2024 22:47:06 +0300 Subject: [PATCH 10/17] Update Phoenix.Endpoint doc regarding the `check_csrf` opt Cannot be disabled with `check_origin` disabled as well --- lib/phoenix/endpoint.ex | 4 ++-- lib/phoenix/socket/transport.ex | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex index d81b8b63ea..66e35324dc 100644 --- a/lib/phoenix/endpoint.ex +++ b/lib/phoenix/endpoint.ex @@ -859,8 +859,8 @@ defmodule Phoenix.Endpoint do The MFA is invoked with the request `%URI{}` as the first argument, followed by arguments in the MFA list, and must return a boolean. - * `:check_csrf` - if the transport should perform CSRF check. If `origin` check is disabled as - well as CSRF check, your app is vulnerable to Cross-Site WebSocket Hijacking (CSWSH) attacks. + * `:check_csrf` - if the transport should perform CSRF check. Note that disabling + both CSRF and origin checks at the same time is not allowed and will raise. Defaults to `true` * `:code_reloader` - enable or disable the code reloader. Defaults to your diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex index 82f1bbb382..7489949ead 100644 --- a/lib/phoenix/socket/transport.ex +++ b/lib/phoenix/socket/transport.ex @@ -463,7 +463,7 @@ defmodule Phoenix.Socket.Transport do * `:user_agent` - the value of the "user-agent" request header - The CSRF check can be disabled with the `:check_csrf` option. + The CSRF check can be disabled by setting the `:check_csrf` option to `false`. """ def connect_info(conn, endpoint, keys, opts \\ []) do for key <- keys, into: %{} do From 5b2ba48e9cbad169bee5b889e17e04441898155c Mon Sep 17 00:00:00 2001 From: Tangui <29804907+tanguilp@users.noreply.github.com> Date: Thu, 24 Oct 2024 23:01:00 +0300 Subject: [PATCH 11/17] Fix doc indentation in endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/phoenix/endpoint.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex index 66e35324dc..186efe3228 100644 --- a/lib/phoenix/endpoint.ex +++ b/lib/phoenix/endpoint.ex @@ -860,8 +860,8 @@ defmodule Phoenix.Endpoint do followed by arguments in the MFA list, and must return a boolean. * `:check_csrf` - if the transport should perform CSRF check. Note that disabling - both CSRF and origin checks at the same time is not allowed and will raise. - Defaults to `true` + both CSRF and origin checks at the same time is not allowed and will raise. + Defaults to `true` * `:code_reloader` - enable or disable the code reloader. Defaults to your endpoint configuration From d339626bd2e7be2198bda6bb34aeb7162453b227 Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Sun, 27 Oct 2024 18:31:05 +0300 Subject: [PATCH 12/17] Check prsence of CSRF or origin check when starting the transports --- lib/phoenix/endpoint/supervisor.ex | 29 +++++++++++++++++++++++--- lib/phoenix/socket/transport.ex | 5 ----- test/phoenix/socket/transport_test.exs | 7 ------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex index 5ee904dcb7..fc1ce37183 100644 --- a/lib/phoenix/endpoint/supervisor.ex +++ b/lib/phoenix/endpoint/supervisor.ex @@ -84,9 +84,9 @@ defmodule Phoenix.Endpoint.Supervisor do children = config_children(mod, secret_conf, default_conf) ++ pubsub_children(mod, conf) ++ - socket_children(mod, :child_spec) ++ + socket_children(otp_app, mod, :child_spec) ++ server_children(mod, conf, server?) ++ - socket_children(mod, :drainer_spec) ++ + socket_children(otp_app, mod, :drainer_spec) ++ watcher_children(mod, conf, server?) Supervisor.init(children, strategy: :one_for_one) @@ -118,10 +118,12 @@ defmodule Phoenix.Endpoint.Supervisor do end end - defp socket_children(endpoint, fun) do + defp socket_children(otp_app, endpoint, fun) do for {_, socket, opts} <- Enum.uniq_by(endpoint.__sockets__(), &elem(&1, 1)), spec = apply_or_ignore(socket, fun, [[endpoint: endpoint] ++ opts]), spec != :ignore do + check_origin_or_csrf_checked!(otp_app, endpoint, opts) + spec end end @@ -135,6 +137,27 @@ defmodule Phoenix.Endpoint.Supervisor do end end + defp check_origin_or_csrf_checked!(otp_app, endpoint, socket_opts) do + endpoint_check_origin = config(otp_app, endpoint)[:check_origin] + + for {transport, transport_opts} <- socket_opts do + check_origin = + if is_boolean(transport_opts[:check_origin]) do + transport_opts[:check_origin] + else + endpoint_check_origin + end + + check_csrf = transport_opts[:check_csrf] + + if check_origin == false and check_csrf == false do + raise ArgumentError, + "One of :check_origin and :check_csrf must be set to non-false value for " <> + "transport #{inspect(transport)}" + end + end + end + defp config_children(mod, conf, default_conf) do args = {mod, conf, default_conf, name: Module.concat(mod, "Config")} [{Phoenix.Config, args}] diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex index 7489949ead..c9ab5dd956 100644 --- a/lib/phoenix/socket/transport.ex +++ b/lib/phoenix/socket/transport.ex @@ -332,13 +332,8 @@ defmodule Phoenix.Socket.Transport do import Plug.Conn origin = conn |> get_req_header("origin") |> List.first() check_origin = check_origin_config(handler, endpoint, opts) - check_csrf = opts[:check_csrf] cond do - check_origin == false and check_csrf == false -> - raise ArgumentError, - "One of :check_origin and :check_csrf must be set" - is_nil(origin) or check_origin == false -> conn diff --git a/test/phoenix/socket/transport_test.exs b/test/phoenix/socket/transport_test.exs index 87caeea22a..05700342dd 100644 --- a/test/phoenix/socket/transport_test.exs +++ b/test/phoenix/socket/transport_test.exs @@ -237,13 +237,6 @@ defmodule Phoenix.Socket.TransportTest do # an allowed host refute check_origin("https://host.com/", check_origin: mfa).halted end - - test "raises if both :check_origin and :check_csrf are set to false" do - assert_raise ArgumentError, ~r/One of :check_origin and :check_csrf must be set/, fn -> - check_origin("https://host.com/", check_origin: false, check_csrf: false) - end - end - end ## Check subprotocols From 0007b6c43521fe873610b1cf8d90e2781c166eb3 Mon Sep 17 00:00:00 2001 From: Tangui <29804907+tanguilp@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:35:06 +0300 Subject: [PATCH 13/17] Fix typo in exception message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/phoenix/endpoint/supervisor.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex index fc1ce37183..6c7d1819d6 100644 --- a/lib/phoenix/endpoint/supervisor.ex +++ b/lib/phoenix/endpoint/supervisor.ex @@ -152,7 +152,7 @@ defmodule Phoenix.Endpoint.Supervisor do if check_origin == false and check_csrf == false do raise ArgumentError, - "One of :check_origin and :check_csrf must be set to non-false value for " <> + "one of :check_origin and :check_csrf must be set to non-false value for " <> "transport #{inspect(transport)}" end end From a912c18d4daa44c5efb85f9590d8b484e0b22410 Mon Sep 17 00:00:00 2001 From: Tangui <29804907+tanguilp@users.noreply.github.com> Date: Mon, 28 Oct 2024 21:30:39 +0300 Subject: [PATCH 14/17] Use Keyword.get/3 instead of if construct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/phoenix/endpoint/supervisor.ex | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex index 6c7d1819d6..922b1e0450 100644 --- a/lib/phoenix/endpoint/supervisor.ex +++ b/lib/phoenix/endpoint/supervisor.ex @@ -142,11 +142,7 @@ defmodule Phoenix.Endpoint.Supervisor do for {transport, transport_opts} <- socket_opts do check_origin = - if is_boolean(transport_opts[:check_origin]) do - transport_opts[:check_origin] - else - endpoint_check_origin - end + Keyword.get(transport_opts, :check_origin, endpoint_check_origin) check_csrf = transport_opts[:check_csrf] From 7018509e66a5b3e7bded506b46eebf53463e124b Mon Sep 17 00:00:00 2001 From: Tangui Le Pense <29804907+tanguilp@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:44:32 +0300 Subject: [PATCH 15/17] Refactor check and add tests --- lib/phoenix/endpoint/supervisor.ex | 18 ++++------ test/phoenix/endpoint/supervisor_test.exs | 40 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex index 922b1e0450..ab73ac800c 100644 --- a/lib/phoenix/endpoint/supervisor.ex +++ b/lib/phoenix/endpoint/supervisor.ex @@ -84,11 +84,10 @@ defmodule Phoenix.Endpoint.Supervisor do children = config_children(mod, secret_conf, default_conf) ++ pubsub_children(mod, conf) ++ - socket_children(otp_app, mod, :child_spec) ++ + socket_children(mod, conf, :child_spec) ++ server_children(mod, conf, server?) ++ - socket_children(otp_app, mod, :drainer_spec) ++ + socket_children(mod, conf, :drainer_spec) ++ watcher_children(mod, conf, server?) - Supervisor.init(children, strategy: :one_for_one) end @@ -118,12 +117,11 @@ defmodule Phoenix.Endpoint.Supervisor do end end - defp socket_children(otp_app, endpoint, fun) do + defp socket_children(endpoint, conf, fun) do for {_, socket, opts} <- Enum.uniq_by(endpoint.__sockets__(), &elem(&1, 1)), + _ = check_origin_or_csrf_checked!(conf, opts), spec = apply_or_ignore(socket, fun, [[endpoint: endpoint] ++ opts]), spec != :ignore do - check_origin_or_csrf_checked!(otp_app, endpoint, opts) - spec end end @@ -137,12 +135,10 @@ defmodule Phoenix.Endpoint.Supervisor do end end - defp check_origin_or_csrf_checked!(otp_app, endpoint, socket_opts) do - endpoint_check_origin = config(otp_app, endpoint)[:check_origin] - - for {transport, transport_opts} <- socket_opts do + defp check_origin_or_csrf_checked!(endpoint_conf, socket_opts) do + for {transport, transport_opts} <- socket_opts, is_list(transport_opts) do check_origin = - Keyword.get(transport_opts, :check_origin, endpoint_check_origin) + Keyword.get(transport_opts, :check_origin, endpoint_conf[:check_origin]) check_csrf = transport_opts[:check_csrf] diff --git a/test/phoenix/endpoint/supervisor_test.exs b/test/phoenix/endpoint/supervisor_test.exs index 235c5c0be4..3b2b958db6 100644 --- a/test/phoenix/endpoint/supervisor_test.exs +++ b/test/phoenix/endpoint/supervisor_test.exs @@ -180,4 +180,44 @@ defmodule Phoenix.Endpoint.SupervisorTest do end) end end + + describe "origin & CSRF checks config" do + defmodule TestSocket do + @behaviour Phoenix.Socket.Transport + def child_spec(_), do: :ignore + def connect(_), do: {:ok, []} + def init(state), do: {:ok, state} + def handle_in(_, state), do: {:ok, state} + def handle_info(_, state), do: {:ok, state} + def terminate(_, _), do: :ok + end + + defmodule SocketEndpoint do + use Phoenix.Endpoint, otp_app: :phoenix + + socket "/ws", TestSocket, websocket: [check_csrf: false, check_origin: false] + end + + Application.put_env(:phoenix, SocketEndpoint, []) + + test "fails when CSRF and origin checks both disabled in transport" do + assert_raise ArgumentError, ~r/one of :check_origin and :check_csrf must be set/, fn -> + Supervisor.init({:phoenix, SocketEndpoint, []}) + end + end + + defmodule SocketEndpointOriginCheckDisabled do + use Phoenix.Endpoint, otp_app: :phoenix + + socket "/ws", TestSocket, websocket: [check_csrf: false] + end + + Application.put_env(:phoenix, SocketEndpointOriginCheckDisabled, check_origin: false) + + test "fails when origin is disabled in endpoint config and CSRF disabled in transport" do + assert_raise ArgumentError, ~r/one of :check_origin and :check_csrf must be set/, fn -> + Supervisor.init({:phoenix, SocketEndpointOriginCheckDisabled, []}) + end + end + end end From 0e2c1cbdeeddb8cdedbc04e2a7dbe762f76147b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 31 Oct 2024 08:31:27 +0100 Subject: [PATCH 16/17] Update lib/phoenix/endpoint/supervisor.ex --- lib/phoenix/endpoint/supervisor.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex index ab73ac800c..597f175d90 100644 --- a/lib/phoenix/endpoint/supervisor.ex +++ b/lib/phoenix/endpoint/supervisor.ex @@ -136,9 +136,10 @@ defmodule Phoenix.Endpoint.Supervisor do end defp check_origin_or_csrf_checked!(endpoint_conf, socket_opts) do + check_origin = endpoint_conf[:check_origin] + for {transport, transport_opts} <- socket_opts, is_list(transport_opts) do - check_origin = - Keyword.get(transport_opts, :check_origin, endpoint_conf[:check_origin]) + check_origin = Keyword.get(transport_opts, :check_origin, check_origin) check_csrf = transport_opts[:check_csrf] From 6acae447c2a43a43ad02179ffc4a55d195674d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 31 Oct 2024 08:36:14 +0100 Subject: [PATCH 17/17] Update lib/phoenix/endpoint.ex --- lib/phoenix/endpoint.ex | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex index 186efe3228..a923be9a84 100644 --- a/lib/phoenix/endpoint.ex +++ b/lib/phoenix/endpoint.ex @@ -859,9 +859,12 @@ defmodule Phoenix.Endpoint do The MFA is invoked with the request `%URI{}` as the first argument, followed by arguments in the MFA list, and must return a boolean. - * `:check_csrf` - if the transport should perform CSRF check. Note that disabling - both CSRF and origin checks at the same time is not allowed and will raise. - Defaults to `true` + * `:check_csrf` - if the transport should perform CSRF check. To avoid + "Cross-Site WebSocket Hijacking", you must have at least one of + `check_origin` and `check_csrf` enabled. If you set both to `false`, + Phoenix will raise, but it is still possible to disable both by passing + a custom MFA to `check_origin`. In such cases, it is your responsibility + to ensure at least one of them is enabled. Defaults to `true` * `:code_reloader` - enable or disable the code reloader. Defaults to your endpoint configuration