Always resolve localhost to loopback address

This always resolves "localhost" to a loopback address which
has security benefits such as preventing a malicious dns server
redirecting local connections and allows software to assume
it is a secure hostname.

This is being adopted by web browsers:

- https://w3c.github.io/webappsec-secure-contexts/
- https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/RC9dSw-O3fE/E3_0XaT0BAAJ
- 8da2a80724
- https://bugs.webkit.org/show_bug.cgi?id=171934
- https://tools.ietf.org/html/draft-west-let-localhost-be-localhost-06
This commit is contained in:
Patrick Griffis 2019-02-05 08:53:44 -05:00 committed by Philip Withnall
parent 01acb8907f
commit ea99872e45
3 changed files with 257 additions and 5 deletions

View File

@ -296,15 +296,50 @@ remove_duplicates (GList *addrs)
} }
} }
static gboolean
hostname_is_localhost (const char *hostname)
{
size_t len = strlen (hostname);
const char *p;
/* Match "localhost", "localhost.", "*.localhost" and "*.localhost." */
if (len < strlen ("localhost"))
return FALSE;
if (hostname[len - 1] == '.')
len--;
/* Scan backwards in @hostname to find the right-most dot (excluding the final dot, if it exists, as it was chopped off above).
* We cant use strrchr() because because we need to operate with string lengths.
* End with @p pointing to the character after the right-most dot. */
p = hostname + len - 1;
while (p >= hostname)
{
if (*p == '.')
{
p++;
break;
}
else if (p == hostname)
break;
p--;
}
len -= p - hostname;
return g_ascii_strncasecmp (p, "localhost", MAX (len, strlen ("localhost"))) == 0;
}
/* Note that this does not follow the "FALSE means @error is set" /* Note that this does not follow the "FALSE means @error is set"
* convention. The return value tells the caller whether it should * convention. The return value tells the caller whether it should
* return @addrs and @error to the caller right away, or if it should * return @addrs and @error to the caller right away, or if it should
* continue and trying to resolve the name as a hostname. * continue and trying to resolve the name as a hostname.
*/ */
static gboolean static gboolean
handle_ip_address (const char *hostname, handle_ip_address_or_localhost (const char *hostname,
GList **addrs, GList **addrs,
GError **error) GResolverNameLookupFlags flags,
GError **error)
{ {
GInetAddress *addr; GInetAddress *addr;
@ -355,6 +390,28 @@ handle_ip_address (const char *hostname,
return TRUE; return TRUE;
} }
/* Always resolve localhost to a loopback address so it can be reliably considered secure.
This behavior is being adopted by browsers:
- https://w3c.github.io/webappsec-secure-contexts/
- https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/RC9dSw-O3fE/E3_0XaT0BAAJ
- https://chromium.googlesource.com/chromium/src.git/+/8da2a80724a9b896890602ff77ef2216cb951399
- https://bugs.webkit.org/show_bug.cgi?id=171934
- https://tools.ietf.org/html/draft-west-let-localhost-be-localhost-06
*/
if (hostname_is_localhost (hostname))
{
if (flags & G_RESOLVER_NAME_LOOKUP_FLAGS_IPV6_ONLY)
*addrs = g_list_append (*addrs, g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV6));
if (flags & G_RESOLVER_NAME_LOOKUP_FLAGS_IPV4_ONLY)
*addrs = g_list_append (*addrs, g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV4));
if (*addrs == NULL)
{
*addrs = g_list_append (*addrs, g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV6));
*addrs = g_list_append (*addrs, g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV4));
}
return TRUE;
}
return FALSE; return FALSE;
} }
@ -374,7 +431,7 @@ lookup_by_name_real (GResolver *resolver,
g_return_val_if_fail (error == NULL || *error == NULL, NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL);
/* Check if @hostname is just an IP address */ /* Check if @hostname is just an IP address */
if (handle_ip_address (hostname, &addrs, error)) if (handle_ip_address_or_localhost (hostname, &addrs, flags, error))
return addrs; return addrs;
if (g_hostname_is_non_ascii (hostname)) if (g_hostname_is_non_ascii (hostname))
@ -513,7 +570,7 @@ lookup_by_name_async_real (GResolver *resolver,
g_return_if_fail (!(flags & G_RESOLVER_NAME_LOOKUP_FLAGS_IPV4_ONLY && flags & G_RESOLVER_NAME_LOOKUP_FLAGS_IPV6_ONLY)); g_return_if_fail (!(flags & G_RESOLVER_NAME_LOOKUP_FLAGS_IPV4_ONLY && flags & G_RESOLVER_NAME_LOOKUP_FLAGS_IPV6_ONLY));
/* Check if @hostname is just an IP address */ /* Check if @hostname is just an IP address */
if (handle_ip_address (hostname, &addrs, &error)) if (handle_ip_address_or_localhost (hostname, &addrs, flags, &error))
{ {
GTask *task; GTask *task;

View File

@ -112,6 +112,12 @@ do_lookup_by_name (GTask *task,
else else
g_task_return_pointer (task, g_list_copy_deep (self->ipv6_results, copy_object, NULL), NULL); g_task_return_pointer (task, g_list_copy_deep (self->ipv6_results, copy_object, NULL), NULL);
} }
else if (flags == G_RESOLVER_NAME_LOOKUP_FLAGS_DEFAULT)
{
/* This is only the minimal implementation needed for some tests */
g_assert (self->ipv4_error == NULL && self->ipv6_error == NULL && self->ipv6_results == NULL);
g_task_return_pointer (task, g_list_copy_deep (self->ipv4_results, copy_object, NULL), NULL);
}
else else
g_assert_not_reached (); g_assert_not_reached ();
} }
@ -130,6 +136,22 @@ lookup_by_name_with_flags_async (GResolver *resolver,
g_object_unref (task); g_object_unref (task);
} }
static GList *
lookup_by_name (GResolver *resolver,
const gchar *hostname,
GCancellable *cancellable,
GError **error)
{
GList *result = NULL;
GTask *task = g_task_new (resolver, cancellable, NULL, NULL);
g_task_set_task_data (task, GUINT_TO_POINTER (G_RESOLVER_NAME_LOOKUP_FLAGS_DEFAULT), NULL);
g_task_run_in_thread_sync (task, do_lookup_by_name);
result = g_task_propagate_pointer (task, error);
g_object_unref (task);
return result;
}
static GList * static GList *
lookup_by_name_with_flags_finish (GResolver *resolver, lookup_by_name_with_flags_finish (GResolver *resolver,
GAsyncResult *result, GAsyncResult *result,
@ -160,6 +182,7 @@ mock_resolver_class_init (MockResolverClass *klass)
GObjectClass *object_class = G_OBJECT_CLASS (klass); GObjectClass *object_class = G_OBJECT_CLASS (klass);
resolver_class->lookup_by_name_with_flags_async = lookup_by_name_with_flags_async; resolver_class->lookup_by_name_with_flags_async = lookup_by_name_with_flags_async;
resolver_class->lookup_by_name_with_flags_finish = lookup_by_name_with_flags_finish; resolver_class->lookup_by_name_with_flags_finish = lookup_by_name_with_flags_finish;
resolver_class->lookup_by_name = lookup_by_name;
object_class->finalize = mock_resolver_finalize; object_class->finalize = mock_resolver_finalize;
} }

View File

@ -418,6 +418,131 @@ test_loopback_sync (void)
g_object_unref (addr); g_object_unref (addr);
} }
static void
test_localhost_sync (void)
{
GSocketConnectable *addr; /* owned */
GSocketAddressEnumerator *enumerator; /* owned */
GSocketAddress *a; /* owned */
GError *error = NULL;
GResolver *original_resolver; /* owned */
MockResolver *mock_resolver; /* owned */
GList *ipv4_results = NULL; /* owned */
/* This test ensures that variations of the "localhost" hostname always resolve to a loopback address */
/* Set up a DNS resolver that returns nonsense for "localhost" */
original_resolver = g_resolver_get_default ();
mock_resolver = mock_resolver_new ();
g_resolver_set_default (G_RESOLVER (mock_resolver));
ipv4_results = g_list_append (ipv4_results, g_inet_address_new_from_string ("123.123.123.123"));
mock_resolver_set_ipv4_results (mock_resolver, ipv4_results);
addr = g_network_address_new ("localhost.", 616);
enumerator = g_socket_connectable_enumerate (addr);
/* IPv6 address. */
a = g_socket_address_enumerator_next (enumerator, NULL, &error);
g_assert_no_error (error);
assert_socket_address_matches (a, "::1", 616);
g_object_unref (a);
/* IPv4 address. */
a = g_socket_address_enumerator_next (enumerator, NULL, &error);
g_assert_no_error (error);
assert_socket_address_matches (a, "127.0.0.1", 616);
g_object_unref (a);
/* End of results. */
g_assert_null (g_socket_address_enumerator_next (enumerator, NULL, &error));
g_assert_no_error (error);
g_object_unref (enumerator);
g_object_unref (addr);
addr = g_network_address_new (".localhost", 616);
enumerator = g_socket_connectable_enumerate (addr);
/* IPv6 address. */
a = g_socket_address_enumerator_next (enumerator, NULL, &error);
g_assert_no_error (error);
assert_socket_address_matches (a, "::1", 616);
g_object_unref (a);
/* IPv4 address. */
a = g_socket_address_enumerator_next (enumerator, NULL, &error);
g_assert_no_error (error);
assert_socket_address_matches (a, "127.0.0.1", 616);
g_object_unref (a);
/* End of results. */
g_assert_null (g_socket_address_enumerator_next (enumerator, NULL, &error));
g_assert_no_error (error);
g_object_unref (enumerator);
g_object_unref (addr);
addr = g_network_address_new ("foo.localhost", 616);
enumerator = g_socket_connectable_enumerate (addr);
/* IPv6 address. */
a = g_socket_address_enumerator_next (enumerator, NULL, &error);
g_assert_no_error (error);
assert_socket_address_matches (a, "::1", 616);
g_object_unref (a);
/* IPv4 address. */
a = g_socket_address_enumerator_next (enumerator, NULL, &error);
g_assert_no_error (error);
assert_socket_address_matches (a, "127.0.0.1", 616);
g_object_unref (a);
/* End of results. */
g_assert_null (g_socket_address_enumerator_next (enumerator, NULL, &error));
g_assert_no_error (error);
g_object_unref (enumerator);
g_object_unref (addr);
addr = g_network_address_new (".localhost.", 616);
enumerator = g_socket_connectable_enumerate (addr);
/* IPv6 address. */
a = g_socket_address_enumerator_next (enumerator, NULL, &error);
g_assert_no_error (error);
assert_socket_address_matches (a, "::1", 616);
g_object_unref (a);
/* IPv4 address. */
a = g_socket_address_enumerator_next (enumerator, NULL, &error);
g_assert_no_error (error);
assert_socket_address_matches (a, "127.0.0.1", 616);
g_object_unref (a);
/* End of results. */
g_assert_null (g_socket_address_enumerator_next (enumerator, NULL, &error));
g_assert_no_error (error);
g_object_unref (enumerator);
g_object_unref (addr);
addr = g_network_address_new ("invalid", 616);
enumerator = g_socket_connectable_enumerate (addr);
/* IPv4 address. */
a = g_socket_address_enumerator_next (enumerator, NULL, &error);
g_assert_no_error (error);
assert_socket_address_matches (a, "123.123.123.123", 616);
g_object_unref (a);
/* End of results. */
g_assert_null (g_socket_address_enumerator_next (enumerator, NULL, &error));
g_assert_no_error (error);
g_object_unref (enumerator);
g_object_unref (addr);
g_resolver_set_default (original_resolver);
g_list_free_full (ipv4_results, (GDestroyNotify) g_object_unref);
g_object_unref (original_resolver);
g_object_unref (mock_resolver);
}
typedef struct { typedef struct {
GList/*<owned GSocketAddress> */ *addrs; /* owned */ GList/*<owned GSocketAddress> */ *addrs; /* owned */
GMainLoop *loop; /* owned */ GMainLoop *loop; /* owned */
@ -536,6 +661,51 @@ test_loopback_async (void)
g_object_unref (addr); g_object_unref (addr);
} }
static void
test_localhost_async (void)
{
GSocketConnectable *addr; /* owned */
GSocketAddressEnumerator *enumerator; /* owned */
AsyncData data = { 0, };
GResolver *original_resolver; /* owned */
MockResolver *mock_resolver; /* owned */
GList *ipv4_results = NULL; /* owned */
/* This test ensures that variations of the "localhost" hostname always resolve to a loopback address */
/* Set up a DNS resolver that returns nonsense for "localhost" */
original_resolver = g_resolver_get_default ();
mock_resolver = mock_resolver_new ();
g_resolver_set_default (G_RESOLVER (mock_resolver));
ipv4_results = g_list_append (ipv4_results, g_inet_address_new_from_string ("123.123.123.123"));
mock_resolver_set_ipv4_results (mock_resolver, ipv4_results);
addr = g_network_address_new ("localhost", 610);
enumerator = g_socket_connectable_enumerate (addr);
/* Get all the addresses. */
data.addrs = NULL;
data.delay_ms = 1;
data.loop = g_main_loop_new (NULL, FALSE);
g_socket_address_enumerator_next_async (enumerator, NULL, got_addr, &data);
g_main_loop_run (data.loop);
/* Check results. */
g_assert_cmpuint (g_list_length (data.addrs), ==, 2);
assert_socket_address_matches (data.addrs->data, "::1", 610);
assert_socket_address_matches (data.addrs->next->data, "127.0.0.1", 610);
g_resolver_set_default (original_resolver);
g_list_free_full (data.addrs, (GDestroyNotify) g_object_unref);
g_list_free_full (ipv4_results, (GDestroyNotify) g_object_unref);
g_object_unref (original_resolver);
g_object_unref (mock_resolver);
g_object_unref (enumerator);
g_object_unref (addr);
g_main_loop_unref (data.loop);
}
static void static void
test_to_string (void) test_to_string (void)
{ {
@ -1054,6 +1224,8 @@ main (int argc, char *argv[])
g_test_add_func ("/network-address/loopback/basic", test_loopback_basic); g_test_add_func ("/network-address/loopback/basic", test_loopback_basic);
g_test_add_func ("/network-address/loopback/sync", test_loopback_sync); g_test_add_func ("/network-address/loopback/sync", test_loopback_sync);
g_test_add_func ("/network-address/loopback/async", test_loopback_async); g_test_add_func ("/network-address/loopback/async", test_loopback_async);
g_test_add_func ("/network-address/localhost/async", test_localhost_async);
g_test_add_func ("/network-address/localhost/sync", test_localhost_sync);
g_test_add_func ("/network-address/to-string", test_to_string); g_test_add_func ("/network-address/to-string", test_to_string);
g_test_add ("/network-address/happy-eyeballs/basic", HappyEyeballsFixture, NULL, g_test_add ("/network-address/happy-eyeballs/basic", HappyEyeballsFixture, NULL,