From 4daf644398e3cf037497bddcc7e9a9b86ea1eace Mon Sep 17 00:00:00 2001
From: Jens Georg <mail@jensge.org>
Date: Mon, 10 May 2021 10:34:36 +0200
Subject: [PATCH] service: Validate host header

Make sure that the host header matches the ip:port of the context.

This is in line with UDA (Host header is required and must match the
location url) and DLNA 7.2.24.1 (All communication has to use ip
addresses and not names)

Prevents DNS rebinding attacs against agains UPnP services

context: Use SoupURI instead of GUri

Do not bump the implicit requirement to GLib 2.66 for this version
---
 libgupnp/gupnp-context-private.h |  8 ++++
 libgupnp/gupnp-context.c         | 63 ++++++++++++++++++++++++++
 libgupnp/gupnp-service.c         | 13 ++++++
 tests/gtest/test-bugs.c          | 78 ++++++++++++++++++++++++++++++--
 4 files changed, 157 insertions(+), 5 deletions(-)

diff --git a/libgupnp/gupnp-context-private.h b/libgupnp/gupnp-context-private.h
index 513d774..9716de3 100644
--- a/libgupnp/gupnp-context-private.h
+++ b/libgupnp/gupnp-context-private.h
@@ -36,6 +36,14 @@ _gupnp_context_add_server_handler_with_data (GUPnPContext *context,
                                              const char *path,
                                              AclServerHandler *data);
 
+G_GNUC_INTERNAL gboolean
+gupnp_context_validate_host_header (GUPnPContext *context, const char *host);
+
+gboolean
+validate_host_header (const char *host_header,
+                      const char *host_ip,
+                      guint context_port);
+
 G_END_DECLS
 
 #endif /* GUPNP_CONTEXT_PRIVATE_H */
diff --git a/libgupnp/gupnp-context.c b/libgupnp/gupnp-context.c
index 022910d..9c99ee2 100644
--- a/libgupnp/gupnp-context.c
+++ b/libgupnp/gupnp-context.c
@@ -1653,3 +1653,66 @@ gupnp_context_rewrite_uri (GUPnPContext *context, const char *plain_uri)
 
         return retval;
 }
+
+gboolean
+validate_host_header (const char *host_header,
+                      const char *host_ip,
+                      guint context_port)
+{
+
+        gboolean retval = FALSE;
+        // Be lazy and let GUri do the heavy lifting here, such as stripping the
+        // [] from v6 addresses, splitting of the port etc.
+        char *uri_from_host = g_strconcat ("http://", host_header, NULL);
+
+        const char *host = NULL;
+        int port = 0;
+
+        SoupURI *uri = soup_uri_new (uri_from_host);
+        if (uri == NULL) {
+                g_debug ("Failed to parse HOST header %s from request",
+                         host_header);
+                goto out;
+        }
+        host = soup_uri_get_host (uri);
+        port = soup_uri_get_port (uri);
+
+
+        // -1 means there was no :port; according to UDA this is allowed and
+        // defaults to 80, the HTTP port then
+        if (soup_uri_uses_default_port (uri)) {
+                port = 80;
+        }
+
+        if (!g_str_equal (host, host_ip)) {
+                g_debug ("Mismatch between host header and host IP (%s, "
+                         "expected: %s)",
+                         host,
+                         host_ip);
+        }
+
+        if (port != context_port) {
+                g_debug ("Mismatch between host header and host port (%d, "
+                         "expected %d)",
+                         port,
+                         context_port);
+        }
+
+        retval = g_str_equal (host, host_ip) && port == context_port;
+
+out:
+        g_clear_pointer (&uri, soup_uri_free);
+        g_free (uri_from_host);
+
+        return retval;
+}
+
+gboolean
+gupnp_context_validate_host_header (GUPnPContext *context,
+                                    const char *host_header)
+{
+        return validate_host_header (
+                host_header,
+                gssdp_client_get_host_ip (GSSDP_CLIENT (context)),
+                gupnp_context_get_port (context));
+}
diff --git a/libgupnp/gupnp-service.c b/libgupnp/gupnp-service.c
index ce25ca5..0095648 100644
--- a/libgupnp/gupnp-service.c
+++ b/libgupnp/gupnp-service.c
@@ -954,6 +954,19 @@ control_server_handler (SoupServer                      *server,
 
         context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (service));
 
+        const char *host_header =
+                soup_message_headers_get_one (msg->request_headers, "Host");
+
+        if (!gupnp_context_validate_host_header (context, host_header)) {
+                g_warning ("Host header mismatch, expected %s:%d, got %s",
+                           gssdp_client_get_host_ip (GSSDP_CLIENT (context)),
+                           gupnp_context_get_port (context),
+                           host_header);
+
+                soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
+                return;
+        }
+
         /* Get action name */
         soap_action = soup_message_headers_get_one (msg->request_headers,
                                                     "SOAPAction");
diff --git a/tests/gtest/test-bugs.c b/tests/gtest/test-bugs.c
index 76d35d6..6f224d2 100644
--- a/tests/gtest/test-bugs.c
+++ b/tests/gtest/test-bugs.c
@@ -25,6 +25,7 @@
 
 #include <libgupnp/gupnp.h>
 #include <libgupnp/gupnp-service-private.h>
+#include <libgupnp/gupnp-context-private.h>
 
 typedef struct _TestBgo678701Service {
     GUPnPServiceProxy parent_instance;
@@ -459,14 +460,81 @@ test_bgo_743233 (void)
     g_object_unref (context);
 }
 
+static void
+test_ggo_24 (void)
+{
+        // IPv4
+        g_assert (
+                validate_host_header ("127.0.0.1:4711", "127.0.0.1", 4711));
+
+        g_assert (
+                validate_host_header ("127.0.0.1", "127.0.0.1", 80));
+
+        g_assert_false (
+                validate_host_header ("example.com", "127.0.0.1", 4711));
+
+        g_assert_false (
+                validate_host_header ("example.com:80", "127.0.0.1", 4711));
+
+        g_assert_false (
+                validate_host_header ("example.com:4711", "127.0.0.1", 4711));
+
+        g_assert_false (
+                validate_host_header ("192.168.1.2:4711", "127.0.0.1", 4711));
+
+        g_assert_false (
+                validate_host_header ("[fe80::01]", "127.0.0.1", 4711));
+
+        // Link ids should not be parsed
+        g_assert_false (
+                validate_host_header ("[fe80::01%1]", "127.0.0.1", 4711));
+
+        g_assert_false (
+                validate_host_header ("[fe80::01%eth0]", "127.0.0.1", 4711));
+
+        // IPv6
+        g_assert (
+                validate_host_header ("[::1]:4711", "::1", 4711));
+
+        g_assert (
+                validate_host_header ("[::1]", "::1", 80));
+
+        // Host header needs to be enclosed in [] even without port
+        g_assert_false (
+                validate_host_header ("::1", "::1", 80));
+
+        g_assert_false (
+                validate_host_header ("example.com", "::1", 4711));
+
+        g_assert_false (
+                validate_host_header ("example.com:80", "::1", 4711));
+
+        g_assert_false (
+                validate_host_header ("example.com:4711", "::1", 4711));
+
+        g_assert_false (
+                validate_host_header ("192.168.1.2:4711", "::1", 4711));
+
+        g_assert_false (
+                validate_host_header ("[fe80::01]", "::1", 4711));
+
+        // Link ids should not be parsed
+        g_assert_false (
+                validate_host_header ("[fe80::01%1]", "fe80::acab", 4711));
+
+        g_assert_false (
+                validate_host_header ("[fe80::01%eth0]", "fe80::acab", 4711));
+}
+
 int
 main (int argc, char *argv[]) {
     g_test_init (&argc, &argv, NULL);
-    g_test_add_func ("/bugs/696762", test_bgo_696762);
-    g_test_add_func ("/bugs/678701", test_bgo_678701);
-    g_test_add_func ("/bugs/690400", test_bgo_690400);
-    g_test_add_func ("/bugs/722696", test_bgo_722696);
-    g_test_add_func ("/bugs/743233", test_bgo_743233);
+    g_test_add_func ("/bugs/bgo/696762", test_bgo_696762);
+    g_test_add_func ("/bugs/bgo/678701", test_bgo_678701);
+    g_test_add_func ("/bugs/bgo/690400", test_bgo_690400);
+    g_test_add_func ("/bugs/bgo/722696", test_bgo_722696);
+    g_test_add_func ("/bugs/bgo/743233", test_bgo_743233);
+    g_test_add_func ("/bugs/ggo/24", test_ggo_24);
 
     return g_test_run ();
 }
-- 
2.26.2

