Repository: phoenixframework/phoenix
Pull request #4632 (2021-12-15 19:06:36)

Support opt-in host origin checking


Patch:
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 543ba62fc..de943ea20 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -798,6 +798,10 @@ defmodule Phoenix.Endpoint do
             "//*.other.com"
           ]
 
+      Or to accept any origin matching the request connection's host, port, and scheme:
+
+          check_origin: :conn
+
       Or a custom MFA function:
 
           check_origin: {MyAppWeb.Auth, :my_check_origin?, []}
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index d609b3999..2f1d113a4 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -353,7 +353,7 @@ defmodule Phoenix.Socket.Transport do
       is_nil(origin) or check_origin == false ->
         conn
 
-      origin_allowed?(check_origin, URI.parse(origin), endpoint) ->
+      origin_allowed?(check_origin, URI.parse(origin), endpoint, conn) ->
         conn
 
       true ->
@@ -567,8 +567,11 @@ defmodule Phoenix.Socket.Transport do
           {module, function, arguments} ->
             {module, function, arguments}
 
+          :conn ->
+            :conn
+
           invalid ->
-            raise ArgumentError, ":check_origin expects a boolean, list of hosts, or MFA tuple, got: #{inspect(invalid)}"
+            raise ArgumentError, ":check_origin expects a boolean, list of hosts, :conn, or MFA tuple, got: #{inspect(invalid)}"
         end
 
       {:cache, check_origin}
@@ -588,13 +591,15 @@ defmodule Phoenix.Socket.Transport do
     end
   end
 
-  defp origin_allowed?({module, function, arguments}, uri, _endpoint),
+  defp origin_allowed?({module, function, arguments}, uri, _endpoint, _conn),
     do: apply(module, function, [uri | arguments])
-  defp origin_allowed?(_check_origin, %{host: nil}, _endpoint),
+  defp origin_allowed?(:conn, uri, _endpoint, %Plug.Conn{} = conn),
+    do: uri.host == conn.host and uri.scheme == conn.scheme and uri.port == conn.port
+  defp origin_allowed?(_check_origin, %{host: nil}, _endpoint, _conn),
     do: false
-  defp origin_allowed?(true, uri, endpoint),
+  defp origin_allowed?(true, uri, endpoint, _conn),
     do: compare?(uri.host, host_to_binary(endpoint.config(:url)[:host]))
-  defp origin_allowed?(check_origin, uri, _endpoint) when is_list(check_origin),
+  defp origin_allowed?(check_origin, uri, _endpoint, _conn) when is_list(check_origin),
     do: origin_allowed?(uri, check_origin)
 
   defp origin_allowed?(uri, allowed_origins) do


Test patch:
diff --git a/test/phoenix/socket/transport_test.exs b/test/phoenix/socket/transport_test.exs
index c4af2c7ea..da8c15b70 100644
--- a/test/phoenix/socket/transport_test.exs
+++ b/test/phoenix/socket/transport_test.exs
@@ -51,11 +51,13 @@ defmodule Phoenix.Socket.TransportTest do
   ## Check origin
 
   describe "check_origin/4" do
-    defp check_origin(origin, opts) do
-      conn = conn(:get, "/") |> put_req_header("origin", origin)
+    defp check_origin(%Plug.Conn{} = conn, origin, opts) do
+      conn = put_req_header(conn, "origin", origin)
       Transport.check_origin(conn, make_ref(), Endpoint, opts)
     end
 
+    defp check_origin(origin, opts), do: check_origin(conn(:get, "/"), origin, opts)
+
     test "does not check origin if disabled" do
       refute check_origin("/", check_origin: false).halted
     end
@@ -131,6 +133,22 @@ defmodule Phoenix.Socket.TransportTest do
       refute check_origin("", check_origin: mfa).halted
     end
 
+    test "checks origin against :host" do
+      conn = %Plug.Conn{conn(:get, "/") | host: "example.com", scheme: "http", port: 80}
+      refute check_origin(conn, "http://example.com", check_origin: :conn).halted
+
+      assert check_origin(conn, "https://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "ws://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "wss://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "http://www.example.com", check_origin: :conn).halted
+      assert check_origin(conn, "http://www.another.com", check_origin: :conn).halted
+
+      conn = %Plug.Conn{conn(:get, "/") | host: "example.com", scheme: "https", port: 443}
+      refute check_origin(conn, "https://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "http://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "https://example.com:4000", check_origin: :conn).halted
+    end
+
     test "does not halt invalid URIs when check_origin is disabled" do
       refute check_origin("file://", check_origin: false).halted
       refute check_origin("null", check_origin: false).halted