Commit message: cookie-jar: bail if hostname is an empty string

There are several other ways to fix the problem with this function, but
skipping over all of the code is probably the simplest.

Fixes #3
CVE: CVE-2018-12910
CWE: CWE-125
Vulnerability description: The get_cookies function in soup-cookie-jar.c in libsoup 2.63.2 allows attackers to have unspecified impact via an empty hostname.

Project: libsoup

soup_cookie_jar_finalize (GObject *object)
{
	SoupCookieJarPrivate *priv =
		soup_cookie_jar_get_instance_private (SOUP_COOKIE_JAR (object));
	GHashTableIter iter;
	gpointer key, value;

	g_hash_table_iter_init (&iter, priv->domains);
	while (g_hash_table_iter_next (&iter, &key, &value))
		soup_cookies_free (value);
	g_hash_table_destroy (priv->domains);
	g_hash_table_destroy (priv->serials);

	G_OBJECT_CLASS (soup_cookie_jar_parent_class)->finalize (object);
}

Project: libsoup

soup_cookie_jar_changed (SoupCookieJar *jar,
			 SoupCookie *old, SoupCookie *new)
{
	SoupCookieJarPrivate *priv = soup_cookie_jar_get_instance_private (jar);

	if (old && old != new)
		g_hash_table_remove (priv->serials, old);
	if (new) {
		priv->serial++;
		g_hash_table_insert (priv->serials, new, GUINT_TO_POINTER (priv->serial));
	}

	if (priv->read_only || !priv->constructed)
		return;

	g_signal_emit (jar, signals[CHANGED], 0, old, new);
}

Project: libsoup

soup_cookie_jar_get_cookie_list (SoupCookieJar *jar, SoupURI *uri, gboolean for_http)
{
	g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
	g_return_val_if_fail (uri != NULL, NULL);

	return get_cookies (jar, uri, for_http, TRUE);
}

Project: libsoup

soup_cookie_jar_is_persistent (SoupCookieJar *jar)
{
	g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), FALSE);

	return SOUP_COOKIE_JAR_GET_CLASS (jar)->is_persistent (jar);
}

Project: libsoup

soup_cookie_jar_request_unqueued (SoupSessionFeature *feature,
				  SoupSession *session,
				  SoupMessage *msg)
{
	g_signal_handlers_disconnect_by_func (msg, process_set_cookie_header, feature);
}

Project: libsoup

soup_cookie_jar_add_cookie_with_first_party (SoupCookieJar *jar, SoupURI *first_party, SoupCookie *cookie)
{
	SoupCookieJarPrivate *priv;

	g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
	g_return_if_fail (first_party != NULL);
	g_return_if_fail (cookie != NULL);

	priv = soup_cookie_jar_get_instance_private (jar);
	if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NEVER) {
		soup_cookie_free (cookie);
		return;
	}

	if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_ALWAYS ||
	    !incoming_cookie_is_third_party (cookie, first_party)) {
		/* will steal or free soup_cookie */
		soup_cookie_jar_add_cookie (jar, cookie);
	} else {
		soup_cookie_free (cookie);
	}
}

Project: libsoup

soup_cookie_jar_add_cookie (SoupCookieJar *jar, SoupCookie *cookie)
{
	SoupCookieJarPrivate *priv;
	GSList *old_cookies, *oc, *last = NULL;
	SoupCookie *old_cookie;

	g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
	g_return_if_fail (cookie != NULL);

	/* Never accept cookies for public domains. */
	if (!g_hostname_is_ip_address (cookie->domain) &&
	    soup_tld_domain_is_public_suffix (cookie->domain)) {
		soup_cookie_free (cookie);
		return;
	}

	priv = soup_cookie_jar_get_instance_private (jar);
	old_cookies = g_hash_table_lookup (priv->domains, cookie->domain);
	for (oc = old_cookies; oc; oc = oc->next) {
		old_cookie = oc->data;
		if (!strcmp (cookie->name, old_cookie->name) &&
		    !g_strcmp0 (cookie->path, old_cookie->path)) {
			if (cookie->expires && soup_date_is_past (cookie->expires)) {
				/* The new cookie has an expired date,
				 * this is the way the the server has
				 * of telling us that we have to
				 * remove the cookie.
				 */
				old_cookies = g_slist_delete_link (old_cookies, oc);
				g_hash_table_insert (priv->domains,
						     g_strdup (cookie->domain),
						     old_cookies);
				soup_cookie_jar_changed (jar, old_cookie, NULL);
				soup_cookie_free (old_cookie);
				soup_cookie_free (cookie);
			} else {
				oc->data = cookie;
				soup_cookie_jar_changed (jar, old_cookie, cookie);
				soup_cookie_free (old_cookie);
			}

			return;
		}
		last = oc;
	}

	/* The new cookie is... a new cookie */
	if (cookie->expires && soup_date_is_past (cookie->expires)) {
		soup_cookie_free (cookie);
		return;
	}

	if (last)
		last->next = g_slist_append (NULL, cookie);
	else {
		old_cookies = g_slist_append (NULL, cookie);
		g_hash_table_insert (priv->domains, g_strdup (cookie->domain),
				     old_cookies);
	}

	soup_cookie_jar_changed (jar, NULL, cookie);
}

Project: libsoup

soup_cookie_jar_save (SoupCookieJar *jar)
{
	/* Does nothing, obsolete */
}

Project: libsoup

soup_cookie_jar_get_cookies (SoupCookieJar *jar, SoupURI *uri,
			     gboolean for_http)
{
	GSList *cookies;

	g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
	g_return_val_if_fail (uri != NULL, NULL);

	cookies = get_cookies (jar, uri, for_http, FALSE);

	if (cookies) {
		char *result = soup_cookies_to_cookie_header (cookies);
		g_slist_free (cookies);

		if (!*result) {
			g_free (result);
			result = NULL;
		}
		return result;
	} else
		return NULL;
}

Project: libsoup

soup_cookie_jar_set_cookie (SoupCookieJar *jar, SoupURI *uri,
			    const char *cookie)
{
	SoupCookie *soup_cookie;
	SoupCookieJarPrivate *priv;

	g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
	g_return_if_fail (uri != NULL);
	g_return_if_fail (cookie != NULL);

	if (!uri->host)
		return;

	priv = soup_cookie_jar_get_instance_private (jar);
	if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NEVER)
		return;

	g_return_if_fail (priv->accept_policy != SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY);

	soup_cookie = soup_cookie_parse (cookie, uri);
	if (soup_cookie) {
		/* will steal or free soup_cookie */
		soup_cookie_jar_add_cookie (jar, soup_cookie);
	}
}

Project: libsoup

soup_cookie_jar_set_cookie_with_first_party (SoupCookieJar *jar,
					     SoupURI *uri,
					     SoupURI *first_party,
					     const char *cookie)
{
	SoupCookie *soup_cookie;

	g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
	g_return_if_fail (uri != NULL);
	g_return_if_fail (first_party != NULL);
	g_return_if_fail (cookie != NULL);

	if (!uri->host)
		return;

	soup_cookie = soup_cookie_parse (cookie, uri);
	if (soup_cookie)
		soup_cookie_jar_add_cookie_with_first_party (jar, first_party, soup_cookie);
}

Project: libsoup

get_cookies (SoupCookieJar *jar, SoupURI *uri, gboolean for_http, gboolean copy_cookies)
{
	SoupCookieJarPrivate *priv;
	GSList *cookies, *domain_cookies;
	char *domain, *cur, *next_domain;
	GSList *new_head, *cookies_to_remove = NULL, *p;

	priv = soup_cookie_jar_get_instance_private (jar);

	if (!uri->host || !uri->host[0])
		return NULL;

	/* The logic here is a little weird, but the plan is that if
	 * uri->host is "www.foo.com", we will end up looking up
	 * cookies for ".www.foo.com", "www.foo.com", ".foo.com", and
	 * ".com", in that order. (Logic stolen from Mozilla.)
	 */
	cookies = NULL;
	domain = cur = g_strdup_printf (".%s", uri->host);
	next_domain = domain + 1;
	do {
		new_head = domain_cookies = g_hash_table_lookup (priv->domains, cur);
		while (domain_cookies) {
			GSList *next = domain_cookies->next;
			SoupCookie *cookie = domain_cookies->data;

			if (cookie->expires && soup_date_is_past (cookie->expires)) {
				cookies_to_remove = g_slist_append (cookies_to_remove,
								    cookie);
				new_head = g_slist_delete_link (new_head, domain_cookies);
				g_hash_table_insert (priv->domains,
						     g_strdup (cur),
						     new_head);
			} else if (soup_cookie_applies_to_uri (cookie, uri) &&
				   (for_http || !cookie->http_only))
				cookies = g_slist_append (cookies, copy_cookies ? soup_cookie_copy (cookie) : cookie);

			domain_cookies = next;
		}
		cur = next_domain;
		if (cur)
			next_domain = strchr (cur + 1, '.');
	} while (cur);
	g_free (domain);

	for (p = cookies_to_remove; p; p = p->next) {
		SoupCookie *cookie = p->data;

		soup_cookie_jar_changed (jar, cookie, NULL);
		soup_cookie_free (cookie);
	}
	g_slist_free (cookies_to_remove);

	return g_slist_sort_with_data (cookies, compare_cookies, jar);
}

Project: libsoup

incoming_cookie_is_third_party (SoupCookie *cookie, SoupURI *first_party)
{
	const char *normalized_cookie_domain;
	const char *cookie_base_domain;
	const char *first_party_base_domain;

	if (first_party == NULL || first_party->host == NULL)
		return TRUE;

	normalized_cookie_domain = normalize_cookie_domain (cookie->domain);
	cookie_base_domain = soup_tld_get_base_domain (normalized_cookie_domain, NULL);
	if (cookie_base_domain == NULL)
		cookie_base_domain = cookie->domain;

	first_party_base_domain = soup_tld_get_base_domain (first_party->host, NULL);
	if (first_party_base_domain == NULL)
		first_party_base_domain = first_party->host;
	return !soup_host_matches_host (cookie_base_domain, first_party_base_domain);
}

Project: libsoup

soup_cookie_jar_real_is_persistent (SoupCookieJar *jar)
{
	return FALSE;
}

Project: libsoup

soup_cookie_jar_class_init (SoupCookieJarClass *jar_class)
{
	GObjectClass *object_class = G_OBJECT_CLASS (jar_class);

	object_class->constructed = soup_cookie_jar_constructed;
	object_class->finalize = soup_cookie_jar_finalize;
	object_class->set_property = soup_cookie_jar_set_property;
	object_class->get_property = soup_cookie_jar_get_property;

	jar_class->is_persistent = soup_cookie_jar_real_is_persistent;

	/**
	 * SoupCookieJar::changed:
	 * @jar: the #SoupCookieJar
	 * @old_cookie: the old #SoupCookie value
	 * @new_cookie: the new #SoupCookie value
	 *
	 * Emitted when @jar changes. If a cookie has been added,
	 * @new_cookie will contain the newly-added cookie and
	 * @old_cookie will be %NULL. If a cookie has been deleted,
	 * @old_cookie will contain the to-be-deleted cookie and
	 * @new_cookie will be %NULL. If a cookie has been changed,
	 * @old_cookie will contain its old value, and @new_cookie its
	 * new value.
	 **/
	signals[CHANGED] =
		g_signal_new ("changed",
			      G_OBJECT_CLASS_TYPE (object_class),
			      G_SIGNAL_RUN_FIRST,
			      G_STRUCT_OFFSET (SoupCookieJarClass, changed),
			      NULL, NULL,
			      NULL,
			      G_TYPE_NONE, 2, 
			      SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE,
			      SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE);

	/**
	 * SOUP_COOKIE_JAR_READ_ONLY:
	 *
	 * Alias for the #SoupCookieJar:read-only property. (Whether
	 * or not the cookie jar is read-only.)
	 **/
	g_object_class_install_property (
		object_class, PROP_READ_ONLY,
		g_param_spec_boolean (SOUP_COOKIE_JAR_READ_ONLY,
				      "Read-only",
				      "Whether or not the cookie jar is read-only",
				      FALSE,
				      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

	/**
	 * SOUP_COOKIE_JAR_ACCEPT_POLICY:
	 *
	 * Alias for the #SoupCookieJar:accept-policy property.
	 *
	 * Since: 2.30
	 */
	/**
	 * SoupCookieJar:accept-policy:
	 *
	 * The policy the jar should follow to accept or reject cookies
	 *
	 * Since: 2.30
	 */
	g_object_class_install_property (
		object_class, PROP_ACCEPT_POLICY,
		g_param_spec_enum (SOUP_COOKIE_JAR_ACCEPT_POLICY,
				   "Accept-policy",
				   "The policy the jar should follow to accept or reject cookies",
				   SOUP_TYPE_COOKIE_JAR_ACCEPT_POLICY,
				   SOUP_COOKIE_JAR_ACCEPT_ALWAYS,
				   G_PARAM_READWRITE));
}

Project: libsoup

soup_cookie_jar_get_property (GObject *object, guint prop_id,
			      GValue *value, GParamSpec *pspec)
{
	SoupCookieJarPrivate *priv =
		soup_cookie_jar_get_instance_private (SOUP_COOKIE_JAR (object));

	switch (prop_id) {
	case PROP_READ_ONLY:
		g_value_set_boolean (value, priv->read_only);
		break;
	case PROP_ACCEPT_POLICY:
		g_value_set_enum (value, priv->accept_policy);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

Project: libsoup

soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface,
				      gpointer interface_data)
{
	feature_interface->request_queued = soup_cookie_jar_request_queued;
	feature_interface->request_unqueued = soup_cookie_jar_request_unqueued;
}

Project: libsoup

compare_cookies (gconstpointer a, gconstpointer b, gpointer jar)
{
	SoupCookie *ca = (SoupCookie *)a;
	SoupCookie *cb = (SoupCookie *)b;
	SoupCookieJarPrivate *priv = soup_cookie_jar_get_instance_private (jar);
	int alen, blen;
	guint aserial, bserial;

	/* "Cookies with longer path fields are listed before cookies
	 * with shorter path field."
	 */
	alen = ca->path ? strlen (ca->path) : 0;
	blen = cb->path ? strlen (cb->path) : 0;
	if (alen != blen)
		return blen - alen;

	/* "Among cookies that have equal length path fields, cookies
	 * with earlier creation dates are listed before cookies with
	 * later creation dates."
	 */
	aserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, ca));
	bserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, cb));
	return aserial - bserial;
}

Project: libsoup

soup_cookie_jar_delete_cookie (SoupCookieJar *jar,
			       SoupCookie    *cookie)
{
	SoupCookieJarPrivate *priv;
	GSList *cookies, *p;

	g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
	g_return_if_fail (cookie != NULL);

	priv = soup_cookie_jar_get_instance_private (jar);

	cookies = g_hash_table_lookup (priv->domains, cookie->domain);
	if (cookies == NULL)
		return;

	for (p = cookies; p; p = p->next ) {
		SoupCookie *c = (SoupCookie*)p->data;
		if (soup_cookie_equal (cookie, c)) {
			cookies = g_slist_delete_link (cookies, p);
			g_hash_table_insert (priv->domains,
					     g_strdup (cookie->domain),
					     cookies);
			soup_cookie_jar_changed (jar, c, NULL);
			soup_cookie_free (c);
			return;
		}
	}
}

Project: libsoup

soup_cookie_jar_all_cookies (SoupCookieJar *jar)
{
	SoupCookieJarPrivate *priv;
	GHashTableIter iter;
	GSList *l = NULL;
	gpointer key, value;

	g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);

	priv = soup_cookie_jar_get_instance_private (jar);

	g_hash_table_iter_init (&iter, priv->domains);

	while (g_hash_table_iter_next (&iter, &key, &value)) {
		GSList *p, *cookies = value;
		for (p = cookies; p; p = p->next)
			l = g_slist_prepend (l, soup_cookie_copy (p->data));
	}

	return l;
}

Project: libsoup

normalize_cookie_domain (const char *domain)
{
	/* Trim any leading dot if present to transform the cookie
         * domain into a valid hostname.
         */
	if (domain != NULL && domain[0] == '.')
		return domain + 1;
	return domain;
}

Project: libsoup

soup_cookie_jar_new (void) 
{
	return g_object_new (SOUP_TYPE_COOKIE_JAR, NULL);
}

Project: libsoup

soup_cookie_jar_set_accept_policy (SoupCookieJar *jar,
				   SoupCookieJarAcceptPolicy policy)
{
	SoupCookieJarPrivate *priv;

	g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));

	priv = soup_cookie_jar_get_instance_private (jar);

	if (priv->accept_policy != policy) {
		priv->accept_policy = policy;
		g_object_notify (G_OBJECT (jar), SOUP_COOKIE_JAR_ACCEPT_POLICY);
	}
}

Project: libsoup

msg_starting_cb (SoupMessage *msg, gpointer feature)
{
	SoupCookieJar *jar = SOUP_COOKIE_JAR (feature);
	char *cookies;

	cookies = soup_cookie_jar_get_cookies (jar, soup_message_get_uri (msg), TRUE);
	if (cookies) {
		soup_message_headers_replace (msg->request_headers,
					      "Cookie", cookies);
		g_free (cookies);
	} else
		soup_message_headers_remove (msg->request_headers, "Cookie");
}

Project: libsoup

process_set_cookie_header (SoupMessage *msg, gpointer user_data)
{
	SoupCookieJar *jar = user_data;
	SoupCookieJarPrivate *priv = soup_cookie_jar_get_instance_private (jar);
	GSList *new_cookies, *nc;

	if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NEVER)
		return;

	new_cookies = soup_cookies_from_response (msg);
	for (nc = new_cookies; nc; nc = nc->next) {
		SoupURI *first_party = soup_message_get_first_party (msg);
		
		if ((priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NO_THIRD_PARTY &&
		     !incoming_cookie_is_third_party (nc->data, first_party)) ||
		    priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_ALWAYS)
			soup_cookie_jar_add_cookie (jar, nc->data);
		else
			soup_cookie_free (nc->data);
	}
	g_slist_free (new_cookies);
}

Project: libsoup

soup_cookie_jar_constructed (GObject *object)
{
	SoupCookieJarPrivate *priv =
		soup_cookie_jar_get_instance_private (SOUP_COOKIE_JAR (object));

	priv->constructed = TRUE;
}

Project: libsoup

soup_cookie_jar_init (SoupCookieJar *jar)
{
	SoupCookieJarPrivate *priv = soup_cookie_jar_get_instance_private (jar);

	priv->domains = g_hash_table_new_full (soup_str_case_hash,
					       soup_str_case_equal,
					       g_free, NULL);
	priv->serials = g_hash_table_new (NULL, NULL);
	priv->accept_policy = SOUP_COOKIE_JAR_ACCEPT_ALWAYS;
}

Project: libsoup

soup_cookie_jar_get_accept_policy (SoupCookieJar *jar)
{
	SoupCookieJarPrivate *priv;

	g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), SOUP_COOKIE_JAR_ACCEPT_ALWAYS);

	priv = soup_cookie_jar_get_instance_private (jar);
	return priv->accept_policy;
}

Project: libsoup

soup_cookie_jar_set_property (GObject *object, guint prop_id,
			      const GValue *value, GParamSpec *pspec)
{
	SoupCookieJarPrivate *priv =
		soup_cookie_jar_get_instance_private (SOUP_COOKIE_JAR (object));

	switch (prop_id) {
	case PROP_READ_ONLY:
		priv->read_only = g_value_get_boolean (value);
		break;
	case PROP_ACCEPT_POLICY:
		priv->accept_policy = g_value_get_enum (value);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

Project: libsoup

soup_cookie_jar_request_queued (SoupSessionFeature *feature,
				SoupSession *session,
				SoupMessage *msg)
{
	g_signal_connect (msg, "starting",
			  G_CALLBACK (msg_starting_cb),
			  feature);

	soup_message_add_header_handler (msg, "got-headers",
					 "Set-Cookie",
					 G_CALLBACK (process_set_cookie_header),
					 feature);
}