CVE-2022-42975: Other (NVD-CWE-Other)
value: socket/transport.ex in Phoenix before 1.6.14 mishandles check_origin wildcarding. NOTE: LiveView applications are unaffected by default because of the presence of a LiveView CSRF token.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 7.5
Published: 2022-10-17T06:15Z
Repository: https://github.com/phoenixframework/phoenix
Language: Other
Files changed: lib/phoenix/socket/transport.ex, test/phoenix/socket/transport_test.exs
Commit date: 2022-10-10 11:13:01 -0400
Commit message: Fix wildcard check_origin vulnerability.

Previously, our documentation points to a wilcard example of:

    check_origin: [
      "//*.other.com"
    ]

Which should allow any subdomain of "other.com", but our comparison
for `"//*.other.com"` would allow `api.any-other.com`, which would
allow an attacker to register a domain with a custom prefix of a target
domain and pass origin checks. This patch ensures the `String.ends_with?`
check includes the subdomain dot prefix.

Who is affected?

Only those using a wildcard check origin are affected, and potential
exploits are limited to allowing unauthenticated channel connections
from a bad host. Because LiveView adds its own csrf token to the
connection by default, LiveView applications with wildcard check origin
would refuse connection under this scenario. Additionally, channel
applications utilizing token based authentication would require the
attacker to also have a valid token to connection from a bad host.
Phoenix channels does not allow access to cookies, so an attacker would
also not be able to pass their own cookies from a bad host.

Diff: diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index 26fa16f5..0cc8ddd2 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -622,11 +622,11 @@ defmodule Phoenix.Socket.Transport do
   end
 
   defp compare_host?(_request_host, nil),
     do: true
   defp compare_host?(request_host, "*." <> allowed_host),
-    do: String.ends_with?(request_host, allowed_host)
+    do: request_host == allowed_host or String.ends_with?(request_host, "." <> allowed_host)
   defp compare_host?(request_host, allowed_host),
     do: request_host == allowed_host
 
   # TODO: Remove this once {:system, env_var} deprecation is removed
   defp host_to_binary({:system, env_var}), do: host_to_binary(System.get_env(env_var))
diff --git a/test/phoenix/socket/transport_test.exs b/test/phoenix/socket/transport_test.exs
index 74f642da..59bb9b31 100644
--- a/test/phoenix/socket/transport_test.exs
+++ b/test/phoenix/socket/transport_test.exs
@@ -83,18 +83,33 @@ defmodule Phoenix.Socket.TransportTest do
 
       conn = check_origin("http://org1.ex.com", check_origin: origins)
       refute conn.halted
       conn = check_origin("https://org1.ex.com", check_origin: origins)
       refute conn.halted
+
+      conn = check_origin("https://ex.com", check_origin: origins)
+      refute conn.halted
+
+      conn = check_origin("https://org1.prefix-ex.com", check_origin: origins)
+      assert conn.halted
     end
 
     test "nested wildcard subdomains" do
       origins = ["http://*.foo.example.com"]
 
       conn = check_origin("http://org1.foo.example.com", check_origin: origins)
       refute conn.halted
 
+      conn = check_origin("http://foo.example.com", check_origin: origins)
+      refute conn.halted
+
+      conn = check_origin("http://bad.example.com", check_origin: origins)
+      assert conn.halted
+
+      conn = check_origin("http://org1.prefix-foo.example.com", check_origin: origins)
+      assert conn.halted
+
       conn = check_origin("http://org1.bar.example.com", check_origin: origins)
       assert conn.halted
       assert conn.status == 403
     end
 

Vulnerable code:     do: String.ends_with?(request_host, allowed_host)
Fixed code:     do: request_host == allowed_host or String.ends_with?(request_host, "." <> allowed_host)

      conn = check_origin("https://ex.com", check_origin: origins)
      refute conn.halted

      conn = check_origin("https://org1.prefix-ex.com", check_origin: origins)
      assert conn.halted
      conn = check_origin("http://foo.example.com", check_origin: origins)
      refute conn.halted

      conn = check_origin("http://bad.example.com", check_origin: origins)
      assert conn.halted

      conn = check_origin("http://org1.prefix-foo.example.com", check_origin: origins)
      assert conn.halted