diff options
author | Patrick Griffis <pgriffis@igalia.com> | 2019-11-12 16:52:57 -0800 |
---|---|---|
committer | Patrick Griffis <pgriffis@igalia.com> | 2020-01-21 14:55:54 -0800 |
commit | d4f8cff260326a4688cf078d0e63fae30633f02c (patch) | |
tree | 3724de95db71a9f370da7507b5e3a2d35e1373a2 | |
parent | 22793953b5bd9174adbe183915f82fe358f868ac (diff) | |
download | libsoup-d4f8cff260326a4688cf078d0e63fae30633f02c.tar.gz |
Expose support for same-site cookies
This adds API for web browsers to set extra information to support
same-site cookies.
Note that usage of SoupSession alone does not provide enough
information to reasonably use these at the moment and require
manually setting the information with the extra context a browser
may have.
-rw-r--r-- | docs/reference/libsoup-2.4-sections.txt | 10 | ||||
-rw-r--r-- | libsoup/soup-cookie-jar-db.c | 18 | ||||
-rw-r--r-- | libsoup/soup-cookie-jar-text.c | 45 | ||||
-rw-r--r-- | libsoup/soup-cookie-jar.c | 103 | ||||
-rw-r--r-- | libsoup/soup-cookie-jar.h | 9 | ||||
-rw-r--r-- | libsoup/soup-cookie.c | 67 | ||||
-rw-r--r-- | libsoup/soup-cookie.h | 20 | ||||
-rw-r--r-- | libsoup/soup-message-private.h | 3 | ||||
-rw-r--r-- | libsoup/soup-message.c | 148 | ||||
-rw-r--r-- | libsoup/soup-message.h | 12 | ||||
-rw-r--r-- | tests/cookies-test.c | 6 | ||||
-rw-r--r-- | tests/meson.build | 1 | ||||
-rw-r--r-- | tests/samesite-test.c | 132 |
13 files changed, 554 insertions, 20 deletions
diff --git a/docs/reference/libsoup-2.4-sections.txt b/docs/reference/libsoup-2.4-sections.txt index ea20cfb2..dac47136 100644 --- a/docs/reference/libsoup-2.4-sections.txt +++ b/docs/reference/libsoup-2.4-sections.txt @@ -41,6 +41,11 @@ SoupMessagePriority soup_message_get_priority soup_message_set_priority <SUBSECTION> +soup_message_get_site_for_cookies +soup_message_set_site_for_cookies +soup_message_get_is_top_level_navigation +soup_message_set_is_top_level_navigation +<SUBSECTION> SOUP_MESSAGE_METHOD SOUP_MESSAGE_URI SOUP_MESSAGE_HTTP_VERSION @@ -924,6 +929,10 @@ soup_cookie_get_secure soup_cookie_set_http_only soup_cookie_get_http_only <SUBSECTION> +SoupSameSitePolicy +soup_cookie_set_same_site_policy +soup_cookie_get_same_site_policy +<SUBSECTION> soup_cookie_applies_to_uri soup_cookie_domain_matches <SUBSECTION> @@ -950,6 +959,7 @@ SoupCookieJar soup_cookie_jar_new soup_cookie_jar_get_cookies soup_cookie_jar_get_cookie_list +soup_cookie_jar_get_cookie_list_with_same_site_info soup_cookie_jar_set_cookie soup_cookie_jar_set_cookie_with_first_party <SUBSECTION> diff --git a/libsoup/soup-cookie-jar-db.c b/libsoup/soup-cookie-jar-db.c index 32f791ff..3e7a7072 100644 --- a/libsoup/soup-cookie-jar-db.c +++ b/libsoup/soup-cookie-jar-db.c @@ -128,9 +128,9 @@ soup_cookie_jar_db_new (const char *filename, gboolean read_only) NULL); } -#define QUERY_ALL "SELECT id, name, value, host, path, expiry, lastAccessed, isSecure, isHttpOnly FROM moz_cookies;" -#define CREATE_TABLE "CREATE TABLE moz_cookies (id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT, path TEXT,expiry INTEGER, lastAccessed INTEGER, isSecure INTEGER, isHttpOnly INTEGER)" -#define QUERY_INSERT "INSERT INTO moz_cookies VALUES(NULL, %Q, %Q, %Q, %Q, %d, NULL, %d, %d);" +#define QUERY_ALL "SELECT id, name, value, host, path, expiry, lastAccessed, isSecure, isHttpOnly, sameSite FROM moz_cookies;" +#define CREATE_TABLE "CREATE TABLE moz_cookies (id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT, path TEXT, expiry INTEGER, lastAccessed INTEGER, isSecure INTEGER, isHttpOnly INTEGER, sameSite INTEGER)" +#define QUERY_INSERT "INSERT INTO moz_cookies VALUES(NULL, %Q, %Q, %Q, %Q, %d, NULL, %d, %d, %d);" #define QUERY_DELETE "DELETE FROM moz_cookies WHERE name=%Q AND host=%Q;" enum { @@ -143,6 +143,7 @@ enum { COL_LAST_ACCESS, COL_SECURE, COL_HTTP_ONLY, + COL_SAME_SITE_POLICY, N_COL, }; @@ -157,6 +158,7 @@ callback (void *data, int argc, char **argv, char **colname) time_t now; int max_age; gboolean http_only = FALSE, secure = FALSE; + SoupSameSitePolicy same_site_policy; now = time (NULL); @@ -172,6 +174,7 @@ callback (void *data, int argc, char **argv, char **colname) http_only = (g_strcmp0 (argv[COL_HTTP_ONLY], "1") == 0); secure = (g_strcmp0 (argv[COL_SECURE], "1") == 0); + same_site_policy = g_ascii_strtoll (argv[COL_SAME_SITE_POLICY], NULL, 0); cookie = soup_cookie_new (name, value, host, path, max_age); @@ -179,6 +182,8 @@ callback (void *data, int argc, char **argv, char **colname) soup_cookie_set_secure (cookie, TRUE); if (http_only) soup_cookie_set_http_only (cookie, TRUE); + if (same_site_policy) + soup_cookie_set_same_site_policy (cookie, same_site_policy); soup_cookie_jar_add_cookie (jar, cookie); @@ -241,6 +246,10 @@ open_db (SoupCookieJar *jar) sqlite3_free (error); } + /* Migrate old DB to include same-site info. We simply always run this as it + will safely handle a column with the same name existing */ + sqlite3_exec (priv->db, "ALTER TABLE moz_cookies ADD COLUMN sameSite INTEGER DEFAULT 0", NULL, NULL, NULL); + return FALSE; } @@ -291,7 +300,8 @@ soup_cookie_jar_db_changed (SoupCookieJar *jar, new_cookie->path, expires, new_cookie->secure, - new_cookie->http_only); + new_cookie->http_only, + soup_cookie_get_same_site_policy (new_cookie)); exec_query_with_try_create_table (priv->db, query, NULL, NULL); sqlite3_free (query); } diff --git a/libsoup/soup-cookie-jar-text.c b/libsoup/soup-cookie-jar-text.c index 38cafdf5..a187ced9 100644 --- a/libsoup/soup-cookie-jar-text.c +++ b/libsoup/soup-cookie-jar-text.c @@ -121,6 +121,34 @@ soup_cookie_jar_text_new (const char *filename, gboolean read_only) NULL); } +static SoupSameSitePolicy +string_to_same_site_policy (const char *string) +{ + if (strcmp (string, "Lax") == 0) + return SOUP_SAME_SITE_POLICY_LAX; + else if (strcmp (string, "Strict") == 0) + return SOUP_SAME_SITE_POLICY_STRICT; + else if (strcmp (string, "None") == 0) + return SOUP_SAME_SITE_POLICY_NONE; + else + g_return_val_if_reached (SOUP_SAME_SITE_POLICY_NONE); +} + +static const char * +same_site_policy_to_string (SoupSameSitePolicy policy) +{ + switch (policy) { + case SOUP_SAME_SITE_POLICY_STRICT: + return "Strict"; + case SOUP_SAME_SITE_POLICY_LAX: + return "Lax"; + case SOUP_SAME_SITE_POLICY_NONE: + return "None"; + } + + g_return_val_if_reached ("None"); +} + static SoupCookie* parse_cookie (char *line, time_t now) { @@ -129,7 +157,8 @@ parse_cookie (char *line, time_t now) gboolean http_only; gulong expire_time; int max_age; - char *host, *path, *secure, *expires, *name, *value; + char *host, *path, *secure, *expires, *name, *value, *samesite = NULL; + gsize result_length; if (g_str_has_prefix (line, "#HttpOnly_")) { http_only = TRUE; @@ -140,7 +169,8 @@ parse_cookie (char *line, time_t now) http_only = FALSE; result = g_strsplit (line, "\t", -1); - if (g_strv_length (result) != 7) + result_length = g_strv_length (result); + if (result_length < 7) goto out; /* Check this first */ @@ -164,8 +194,14 @@ parse_cookie (char *line, time_t now) name = result[5]; value = result[6]; + if (result_length == 8) + samesite = result[7]; + cookie = soup_cookie_new (name, value, host, path, max_age); + if (samesite != NULL) + soup_cookie_set_same_site_policy (cookie, string_to_same_site_policy (samesite)); + if (strcmp (secure, "FALSE") != 0) soup_cookie_set_secure (cookie, TRUE); if (http_only) @@ -219,7 +255,7 @@ write_cookie (FILE *out, SoupCookie *cookie) { fseek (out, 0, SEEK_END); - fprintf (out, "%s%s\t%s\t%s\t%s\t%lu\t%s\t%s\n", + fprintf (out, "%s%s\t%s\t%s\t%s\t%lu\t%s\t%s\t%s\n", cookie->http_only ? "#HttpOnly_" : "", cookie->domain, *cookie->domain == '.' ? "TRUE" : "FALSE", @@ -227,7 +263,8 @@ write_cookie (FILE *out, SoupCookie *cookie) cookie->secure ? "TRUE" : "FALSE", (gulong)soup_date_to_time_t (cookie->expires), cookie->name, - cookie->value); + cookie->value, + same_site_policy_to_string (soup_cookie_get_same_site_policy (cookie))); } static void diff --git a/libsoup/soup-cookie-jar.c b/libsoup/soup-cookie-jar.c index 675f87c8..d7b2d7dc 100644 --- a/libsoup/soup-cookie-jar.c +++ b/libsoup/soup-cookie-jar.c @@ -12,6 +12,7 @@ #include <string.h> #include "soup-cookie-jar.h" +#include "soup-message-private.h" #include "soup-misc-private.h" #include "soup.h" @@ -299,8 +300,42 @@ compare_cookies (gconstpointer a, gconstpointer b, gpointer jar) return aserial - bserial; } +static gboolean +cookie_is_valid_for_same_site_policy (SoupCookie *cookie, + const char *method, + SoupURI *uri, + SoupURI *top_level, + SoupURI *cookie_uri, + gboolean is_top_level_navigation, + gboolean for_http) +{ + SoupSameSitePolicy policy = soup_cookie_get_same_site_policy (cookie); + + if (policy == SOUP_SAME_SITE_POLICY_NONE) + return TRUE; + + if (top_level == NULL) + return TRUE; + + if (policy == SOUP_SAME_SITE_POLICY_LAX && is_top_level_navigation && + (SOUP_METHOD_IS_SAFE (method) || for_http == FALSE)) + return TRUE; + + if (is_top_level_navigation && cookie_uri == NULL) + return FALSE; + + return soup_host_matches_host (soup_uri_get_host (cookie_uri ? cookie_uri : top_level), soup_uri_get_host (uri)); +} + static GSList * -get_cookies (SoupCookieJar *jar, SoupURI *uri, gboolean for_http, gboolean copy_cookies) +get_cookies (SoupCookieJar *jar, + SoupURI *uri, + SoupURI *top_level, + SoupURI *site_for_cookies, + const char *method, + gboolean for_http, + gboolean is_top_level_navigation, + gboolean copy_cookies) { SoupCookieJarPrivate *priv; GSList *cookies, *domain_cookies; @@ -334,6 +369,9 @@ get_cookies (SoupCookieJar *jar, SoupURI *uri, gboolean for_http, gboolean copy_ g_strdup (cur), new_head); } else if (soup_cookie_applies_to_uri (cookie, uri) && + cookie_is_valid_for_same_site_policy (cookie, method, uri, top_level, + site_for_cookies, is_top_level_navigation, + for_http) && (for_http || !cookie->http_only)) cookies = g_slist_append (cookies, copy_cookies ? soup_cookie_copy (cookie) : cookie); @@ -388,7 +426,7 @@ soup_cookie_jar_get_cookies (SoupCookieJar *jar, SoupURI *uri, 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); + cookies = get_cookies (jar, uri, NULL, NULL, NULL, for_http, FALSE, FALSE); if (cookies) { char *result = soup_cookies_to_cookie_header (cookies); @@ -432,7 +470,46 @@ soup_cookie_jar_get_cookie_list (SoupCookieJar *jar, SoupURI *uri, gboolean for_ 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); + return get_cookies (jar, uri, NULL, NULL, NULL, for_http, FALSE, TRUE); +} + +/** + * soup_cookie_jar_get_cookie_list_with_same_site_info: + * @jar: a #SoupCookieJar + * @uri: a #SoupURI + * @top_level: (nullable): a #SoupURI for the top level document + * @site_for_cookies: (nullable): a #SoupURI indicating the origin to get cookies for + * @method: (nullable): the HTTP method requesting the cookies, this + * should only be %NULL when @for_http is %FALSE + * @for_http: whether or not the return value is being passed directly + * to an HTTP operation + * @is_top_level_navigation: whether or not the HTTP request is part of + * top level navigation + * + * This is an extended version of soup_cookie_jar_get_cookie_list() that + * provides more information required to use SameSite cookies. See the + * [SameSite cookies spec](https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00) + * for more detailed information. + * + * Return value: (transfer full) (element-type Soup.Cookie): a #GSList + * with the cookies in the @jar that would be sent with a request to @uri. + * + * Since: 2.70 + */ +GSList * +soup_cookie_jar_get_cookie_list_with_same_site_info (SoupCookieJar *jar, + SoupURI *uri, + SoupURI *top_level, + SoupURI *site_for_cookies, + const char *method, + gboolean for_http, + gboolean is_top_level_navigation) +{ + g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL); + g_return_val_if_fail (uri != NULL, NULL); + g_return_val_if_fail (method != NULL || for_http == FALSE, NULL); + + return get_cookies (jar, uri, top_level, site_for_cookies, g_intern_string (method), for_http, is_top_level_navigation, TRUE); } static const char * @@ -724,15 +801,21 @@ static void msg_starting_cb (SoupMessage *msg, gpointer feature) { SoupCookieJar *jar = SOUP_COOKIE_JAR (feature); - char *cookies; + GSList *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 + cookies = soup_cookie_jar_get_cookie_list_with_same_site_info (jar, soup_message_get_uri (msg), + soup_message_get_first_party (msg), + soup_message_get_site_for_cookies (msg), + msg->method, + TRUE, soup_message_get_is_top_level_navigation (msg)); + if (cookies != NULL) { + char *cookie_header = soup_cookies_to_cookie_header (cookies); + soup_message_headers_replace (msg->request_headers, "Cookie", cookie_header); + g_free (cookie_header); + g_slist_free_full (cookies, (GDestroyNotify)soup_cookie_free); + } else { soup_message_headers_remove (msg->request_headers, "Cookie"); + } } static void diff --git a/libsoup/soup-cookie-jar.h b/libsoup/soup-cookie-jar.h index d3ee4f23..639df342 100644 --- a/libsoup/soup-cookie-jar.h +++ b/libsoup/soup-cookie-jar.h @@ -59,6 +59,15 @@ SOUP_AVAILABLE_IN_2_40 GSList * soup_cookie_jar_get_cookie_list (SoupCookieJar *jar, SoupURI *uri, gboolean for_http); +SOUP_AVAILABLE_IN_2_70 +GSList * soup_cookie_jar_get_cookie_list_with_same_site_info ( + SoupCookieJar *jar, + SoupURI *uri, + SoupURI *top_level, + SoupURI *site_for_cookies, + const char *method, + gboolean for_http, + gboolean is_top_level_navigation); SOUP_AVAILABLE_IN_2_24 void soup_cookie_jar_set_cookie (SoupCookieJar *jar, SoupURI *uri, diff --git a/libsoup/soup-cookie.c b/libsoup/soup-cookie.c index 7cea82e5..cf538831 100644 --- a/libsoup/soup-cookie.c +++ b/libsoup/soup-cookie.c @@ -88,6 +88,7 @@ soup_cookie_copy (SoupCookie *cookie) copy->expires = soup_date_copy(cookie->expires); copy->secure = cookie->secure; copy->http_only = cookie->http_only; + soup_cookie_set_same_site_policy (copy, soup_cookie_get_same_site_policy (cookie)); return copy; } @@ -238,6 +239,18 @@ parse_one_cookie (const char *header, SoupURI *origin) cookie->secure = TRUE; if (has_value) parse_value (&p, FALSE); + } else if (MATCH_NAME ("samesite")) { + if (has_value) { + char *policy = parse_value (&p, TRUE); + if (g_ascii_strcasecmp (policy, "Lax") == 0) + soup_cookie_set_same_site_policy (cookie, SOUP_SAME_SITE_POLICY_LAX); + else if (g_ascii_strcasecmp (policy, "Strict") == 0) + soup_cookie_set_same_site_policy (cookie, SOUP_SAME_SITE_POLICY_STRICT); + /* There is an explicit "None" value which is the default. */ + g_free (policy); + } + /* Note that earlier versions of the same-site RFC treated invalid values as strict but + the latest revision simply ignores them. */ } else { /* Ignore unknown attributes, but we still have * to skip over the value. @@ -708,6 +721,8 @@ soup_cookie_set_http_only (SoupCookie *cookie, gboolean http_only) static void serialize_cookie (SoupCookie *cookie, GString *header, gboolean set_cookie) { + SoupSameSitePolicy same_site_policy; + if (!*cookie->name && !*cookie->value) return; @@ -743,12 +758,63 @@ serialize_cookie (SoupCookie *cookie, GString *header, gboolean set_cookie) g_string_append (header, "; domain="); g_string_append (header, cookie->domain); } + + same_site_policy = soup_cookie_get_same_site_policy (cookie); + if (same_site_policy != SOUP_SAME_SITE_POLICY_NONE) { + g_string_append (header, "; SameSite="); + if (same_site_policy == SOUP_SAME_SITE_POLICY_LAX) + g_string_append (header, "Lax"); + else + g_string_append (header, "Strict"); + } if (cookie->secure) g_string_append (header, "; secure"); if (cookie->http_only) g_string_append (header, "; HttpOnly"); } +static const char *same_site_policy_string = "soup-same-site-policy"; +#define SAME_SITE_POLICY_QUARK (g_quark_from_static_string (same_site_policy_string)) + +/** + * soup_cookie_set_same_site_policy: + * @cookie: a #SoupCookie + * @policy: a #SoupSameSitePolicy + * + * When used in conjunction with soup_cookie_jar_get_cookie_list_with_same_site_info() this + * sets the policy of when this cookie should be exposed. + * + * Since: 2.70 + **/ +void +soup_cookie_set_same_site_policy (SoupCookie *cookie, + SoupSameSitePolicy policy) +{ + switch (policy) { + case SOUP_SAME_SITE_POLICY_NONE: + case SOUP_SAME_SITE_POLICY_STRICT: + case SOUP_SAME_SITE_POLICY_LAX: + g_dataset_id_set_data (cookie, SAME_SITE_POLICY_QUARK, GUINT_TO_POINTER (policy)); + break; + default: + g_return_if_reached (); + } +} + +/** + * soup_cookie_get_same_site_policy: + * @cookie: a #SoupCookie + * + * Returns: a #SoupSameSitePolicy + * + * Since: 2.70 + **/ +SoupSameSitePolicy +soup_cookie_get_same_site_policy (SoupCookie *cookie) +{ + return GPOINTER_TO_UINT (g_dataset_id_get_data (cookie, SAME_SITE_POLICY_QUARK)); +} + /** * soup_cookie_to_set_cookie_header: * @cookie: a #SoupCookie @@ -808,6 +874,7 @@ soup_cookie_free (SoupCookie *cookie) g_free (cookie->path); g_clear_pointer (&cookie->expires, soup_date_free); + g_dataset_destroy (cookie); g_slice_free (SoupCookie, cookie); } diff --git a/libsoup/soup-cookie.h b/libsoup/soup-cookie.h index 21973a49..c00a153d 100644 --- a/libsoup/soup-cookie.h +++ b/libsoup/soup-cookie.h @@ -10,6 +10,20 @@ G_BEGIN_DECLS +/** + * SoupSameSitePolicy: + * @SOUP_SAME_SITE_POLICY_NONE: The cookie is exposed with both cross-site and same-site requests + * @SOUP_SAME_SITE_POLICY_LAX: The cookie is withheld on cross-site requests but exposed on cross-site navigations + * @SOUP_SAME_SITE_POLICY_STRICT: The cookie is only exposed for same-site requests + * + * Since: 2.70 + */ +typedef enum { + SOUP_SAME_SITE_POLICY_NONE, + SOUP_SAME_SITE_POLICY_LAX, + SOUP_SAME_SITE_POLICY_STRICT, +} SoupSameSitePolicy; + struct _SoupCookie { char *name; char *value; @@ -80,6 +94,12 @@ SOUP_AVAILABLE_IN_2_24 void soup_cookie_set_http_only (SoupCookie *cookie, gboolean http_only); +SOUP_AVAILABLE_IN_2_70 +void soup_cookie_set_same_site_policy (SoupCookie *cookie, + SoupSameSitePolicy policy); +SOUP_AVAILABLE_IN_2_70 +SoupSameSitePolicy soup_cookie_get_same_site_policy (SoupCookie *cookie); + SOUP_AVAILABLE_IN_2_24 char *soup_cookie_to_set_cookie_header (SoupCookie *cookie); SOUP_AVAILABLE_IN_2_24 diff --git a/libsoup/soup-message-private.h b/libsoup/soup-message-private.h index dd345bd1..0c6f5d97 100644 --- a/libsoup/soup-message-private.h +++ b/libsoup/soup-message-private.h @@ -36,6 +36,7 @@ typedef struct { GSList *disabled_features; SoupURI *first_party; + SoupURI *site_for_cookies; GTlsCertificate *tls_certificate; GTlsCertificateFlags tls_errors; @@ -43,6 +44,8 @@ typedef struct { SoupRequest *request; SoupMessagePriority priority; + + gboolean is_top_level_navigation; } SoupMessagePrivate; void soup_message_cleanup_response (SoupMessage *msg); diff --git a/libsoup/soup-message.c b/libsoup/soup-message.c index 90ccefe9..519972a8 100644 --- a/libsoup/soup-message.c +++ b/libsoup/soup-message.c @@ -144,6 +144,8 @@ enum { PROP_TLS_CERTIFICATE, PROP_TLS_ERRORS, PROP_PRIORITY, + PROP_SITE_FOR_COOKIES, + PROP_IS_TOP_LEVEL_NAVIGATION, LAST_PROP }; @@ -174,6 +176,7 @@ soup_message_finalize (GObject *object) g_clear_pointer (&priv->uri, soup_uri_free); g_clear_pointer (&priv->first_party, soup_uri_free); + g_clear_pointer (&priv->site_for_cookies, soup_uri_free); g_clear_object (&priv->addr); g_clear_object (&priv->auth); @@ -207,6 +210,12 @@ soup_message_set_property (GObject *object, guint prop_id, case PROP_URI: soup_message_set_uri (msg, g_value_get_boxed (value)); break; + case PROP_SITE_FOR_COOKIES: + soup_message_set_site_for_cookies (msg, g_value_get_boxed (value)); + break; + case PROP_IS_TOP_LEVEL_NAVIGATION: + soup_message_set_is_top_level_navigation (msg, g_value_get_boolean (value)); + break; case PROP_HTTP_VERSION: soup_message_set_http_version (msg, g_value_get_enum (value)); break; @@ -270,6 +279,12 @@ soup_message_get_property (GObject *object, guint prop_id, case PROP_URI: g_value_set_boxed (value, priv->uri); break; + case PROP_SITE_FOR_COOKIES: + g_value_set_boxed (value, priv->site_for_cookies); + break; + case PROP_IS_TOP_LEVEL_NAVIGATION: + g_value_set_boolean (value, priv->is_top_level_navigation); + break; case PROP_HTTP_VERSION: g_value_set_enum (value, priv->http_version); break; @@ -812,6 +827,34 @@ soup_message_class_init (SoupMessageClass *message_class) G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); /** + * SoupMessage:site-for-cookkies: + * + * Site used to compare cookies against. Used for SameSite cookie support. + * + * Since: 2.70 + */ + g_object_class_install_property ( + object_class, PROP_SITE_FOR_COOKIES, + g_param_spec_boxed (SOUP_MESSAGE_SITE_FOR_COOKIES, + "Site for cookies", + "The URI for the site to compare cookies against", + SOUP_TYPE_URI, + G_PARAM_READWRITE)); + /** + * SoupMessage:is-top-level-navigation: + * + * Set when the message is navigating between top level domains. + * + * Since: 2.70 + */ + g_object_class_install_property ( + object_class, PROP_IS_TOP_LEVEL_NAVIGATION, + g_param_spec_boolean (SOUP_MESSAGE_IS_TOP_LEVEL_NAVIGATION, + "Is top-level navigation", + "If the current messsage is navigating between top-levels", + FALSE, + G_PARAM_READWRITE)); + /** * SOUP_MESSAGE_REQUEST_BODY: * * Alias for the #SoupMessage:request-body property. (The @@ -1956,6 +1999,111 @@ soup_message_set_first_party (SoupMessage *msg, g_object_notify (G_OBJECT (msg), SOUP_MESSAGE_FIRST_PARTY); } +/** + * soup_message_get_site_for_cookies: + * @msg: a #SoupMessage + * + * Gets @msg's site for cookies #SoupURI + * + * Returns: (transfer none): the @msg's site for cookies #SoupURI + * + * Since: 2.70 + **/ +SoupURI * +soup_message_get_site_for_cookies (SoupMessage *msg) +{ + SoupMessagePrivate *priv; + + g_return_val_if_fail (SOUP_IS_MESSAGE (msg), NULL); + + priv = soup_message_get_instance_private (msg); + return priv->site_for_cookies; +} + +/** + * soup_message_set_site_for_cookies: + * @msg: a #SoupMessage + * @site_for_cookies: (nullable): the #SoupURI for the @msg's site for cookies + * + * Sets @site_for_cookies as the policy URL for same-site cookies for @msg. + * + * It is either the URL of the top-level document or %NULL depending on whether the registrable + * domain of this document's URL matches the registrable domain of its parent's/opener's + * URL. For the top-level document it is set to the document's URL. + * + * See the [same-site spec](https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00) + * for more information. + * + * Since: 2.70 + **/ +void +soup_message_set_site_for_cookies (SoupMessage *msg, + SoupURI *site_for_cookies) +{ + SoupMessagePrivate *priv; + + g_return_if_fail (SOUP_IS_MESSAGE (msg)); + + priv = soup_message_get_instance_private (msg); + + if (priv->site_for_cookies == site_for_cookies) + return; + + if (priv->site_for_cookies) { + if (site_for_cookies && soup_uri_equal (priv->site_for_cookies, site_for_cookies)) + return; + + soup_uri_free (priv->site_for_cookies); + } + + priv->site_for_cookies = site_for_cookies ? soup_uri_copy (site_for_cookies) : NULL; + g_object_notify (G_OBJECT (msg), SOUP_MESSAGE_SITE_FOR_COOKIES); +} + +/** + * soup_message_set_is_top_level_navigation: + * @msg: a #SoupMessage + * @is_top_level_navigation: if %TRUE indicate the current request is a top-level navigation + * + * See the [same-site spec](https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00) + * for more information. + * + * Since: 2.70 + **/ +void +soup_message_set_is_top_level_navigation (SoupMessage *msg, + gboolean is_top_level_navigation) +{ + SoupMessagePrivate *priv; + + g_return_if_fail (SOUP_IS_MESSAGE (msg)); + + priv = soup_message_get_instance_private (msg); + + if (priv->is_top_level_navigation == is_top_level_navigation) + return; + + priv->is_top_level_navigation = is_top_level_navigation; + g_object_notify (G_OBJECT (msg), SOUP_MESSAGE_IS_TOP_LEVEL_NAVIGATION); +} + +/** + * soup_message_get_is_top_level_navigation: + * @msg: a #SoupMessage + * + * Since: 2.70 + **/ +gboolean +soup_message_get_is_top_level_navigation (SoupMessage *msg) +{ + SoupMessagePrivate *priv; + + g_return_val_if_fail (SOUP_IS_MESSAGE (msg), FALSE); + + priv = soup_message_get_instance_private (msg); + return priv->is_top_level_navigation; +} + void soup_message_set_https_status (SoupMessage *msg, SoupConnection *conn) { diff --git a/libsoup/soup-message.h b/libsoup/soup-message.h index 93778961..41004fd9 100644 --- a/libsoup/soup-message.h +++ b/libsoup/soup-message.h @@ -69,6 +69,7 @@ GType soup_message_get_type (void); #define SOUP_MESSAGE_STATUS_CODE "status-code" #define SOUP_MESSAGE_REASON_PHRASE "reason-phrase" #define SOUP_MESSAGE_FIRST_PARTY "first-party" +#define SOUP_MESSAGE_SITE_FOR_COOKIES "site-for-cookies" #define SOUP_MESSAGE_REQUEST_BODY "request-body" #define SOUP_MESSAGE_REQUEST_BODY_DATA "request-body-data" #define SOUP_MESSAGE_REQUEST_HEADERS "request-headers" @@ -78,6 +79,7 @@ GType soup_message_get_type (void); #define SOUP_MESSAGE_TLS_CERTIFICATE "tls-certificate" #define SOUP_MESSAGE_TLS_ERRORS "tls-errors" #define SOUP_MESSAGE_PRIORITY "priority" +#define SOUP_MESSAGE_IS_TOP_LEVEL_NAVIGATION "is-top-level-navigation" SOUP_AVAILABLE_IN_2_4 SoupMessage *soup_message_new (const char *method, @@ -126,6 +128,16 @@ SoupURI *soup_message_get_first_party (SoupMessage *msg); SOUP_AVAILABLE_IN_2_30 void soup_message_set_first_party (SoupMessage *msg, SoupURI *first_party); +SOUP_AVAILABLE_IN_2_70 +SoupURI *soup_message_get_site_for_cookies (SoupMessage *msg); +SOUP_AVAILABLE_IN_2_70 +void soup_message_set_site_for_cookies (SoupMessage *msg, + SoupURI *site_for_cookies); +SOUP_AVAILABLE_IN_2_70 +void soup_message_set_is_top_level_navigation (SoupMessage *msg, + gboolean is_top_level_navigation); +SOUP_AVAILABLE_IN_2_70 +gboolean soup_message_get_is_top_level_navigation (SoupMessage *msg); typedef enum { SOUP_MESSAGE_NO_REDIRECT = (1 << 1), diff --git a/tests/cookies-test.c b/tests/cookies-test.c index 1c07b038..d25da0e9 100644 --- a/tests/cookies-test.c +++ b/tests/cookies-test.c @@ -278,13 +278,13 @@ do_cookies_parsing_test (void) msg = soup_message_new_from_uri ("GET", first_party_uri); soup_message_headers_append (msg->request_headers, "Echo-Set-Cookie", - "two=2; HttpOnly; max-age=100"); + "two=2; HttpOnly; max-age=100; SameSite=Invalid"); soup_session_send_message (session, msg); g_object_unref (msg); msg = soup_message_new_from_uri ("GET", first_party_uri); soup_message_headers_append (msg->request_headers, "Echo-Set-Cookie", - "three=3; httpONLY=Wednesday; max-age=100"); + "three=3; httpONLY=Wednesday; max-age=100; SameSite=Lax"); soup_session_send_message (session, msg); g_object_unref (msg); @@ -302,10 +302,12 @@ do_cookies_parsing_test (void) got2 = TRUE; g_assert_true (soup_cookie_get_http_only (cookie)); g_assert_true (soup_cookie_get_expires (cookie) != NULL); + g_assert_cmpint (soup_cookie_get_same_site_policy (cookie), ==, SOUP_SAME_SITE_POLICY_NONE); } else if (!strcmp (soup_cookie_get_name (cookie), "three")) { got3 = TRUE; g_assert_true (soup_cookie_get_http_only (cookie)); g_assert_true (soup_cookie_get_expires (cookie) != NULL); + g_assert_cmpint (soup_cookie_get_same_site_policy (cookie), ==, SOUP_SAME_SITE_POLICY_LAX); } else { soup_test_assert (FALSE, "got unexpected cookie '%s'", soup_cookie_get_name (cookie)); diff --git a/tests/meson.build b/tests/meson.build index 5d6a8f6b..119bf166 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -44,6 +44,7 @@ tests = [ ['redirect', true, []], ['requester', true, []], ['resource', true, []], + ['samesite', true, []], ['session', true, []], ['server-auth', true, []], ['server', true, []], diff --git a/tests/samesite-test.c b/tests/samesite-test.c new file mode 100644 index 00000000..675f1e57 --- /dev/null +++ b/tests/samesite-test.c @@ -0,0 +1,132 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ + +#include "test-utils.h" + +typedef struct { + SoupURI *origin_uri; + SoupURI *cross_uri; + SoupCookieJar *jar; + GSList *cookies; +} SameSiteFixture; + +static void +same_site_setup (SameSiteFixture *fixture, + gconstpointer data) +{ + SoupCookie *cookie_none, *cookie_lax, *cookie_strict; + + fixture->origin_uri = soup_uri_new ("http://127.0.0.1"); + fixture->cross_uri = soup_uri_new ("http://localhost"); + fixture->jar = soup_cookie_jar_new (); + + cookie_none = soup_cookie_new ("none", "1", "127.0.0.1", "/", 1000); + cookie_lax = soup_cookie_new ("lax", "1", "127.0.0.1", "/", 1000); + soup_cookie_set_same_site_policy (cookie_lax, SOUP_SAME_SITE_POLICY_LAX); + cookie_strict = soup_cookie_new ("strict", "1", "127.0.0.1", "/", 1000); + soup_cookie_set_same_site_policy (cookie_strict, SOUP_SAME_SITE_POLICY_STRICT); + + soup_cookie_jar_add_cookie_with_first_party (fixture->jar, fixture->origin_uri, cookie_none); + soup_cookie_jar_add_cookie_with_first_party (fixture->jar, fixture->origin_uri, cookie_lax); + soup_cookie_jar_add_cookie_with_first_party (fixture->jar, fixture->origin_uri, cookie_strict); +} + +static void +same_site_teardown (SameSiteFixture *fixture, + gconstpointer data) +{ + g_object_unref (fixture->jar); + soup_uri_free (fixture->origin_uri); + soup_uri_free (fixture->cross_uri); + g_slist_free_full (fixture->cookies, (GDestroyNotify) soup_cookie_free); +} + +static void +assert_highest_policy_visible (GSList *cookies, SoupSameSitePolicy policy) +{ + GSList *l; + size_t size = 0, expected_count; + for (l = cookies; l; l = l->next) { + g_assert_cmpint (soup_cookie_get_same_site_policy (l->data), <=, policy); + ++size; + } + + switch (policy) { + case SOUP_SAME_SITE_POLICY_STRICT: + expected_count = 3; + break; + case SOUP_SAME_SITE_POLICY_LAX: + expected_count = 2; + break; + case SOUP_SAME_SITE_POLICY_NONE: + expected_count = 1; + break; + } + + g_assert_cmpuint (size, ==, expected_count); +} + +typedef struct { + const char *name; + gboolean cross_origin; + gboolean cookie_uri_is_origin; + gboolean top_level_nav; + gboolean javascript; + gboolean unsafe_method; + SoupSameSitePolicy visible_policy; +} SameSiteTest; + +static void +same_site_test (SameSiteFixture *fixture, gconstpointer user_data) +{ + const SameSiteTest *test = user_data; + fixture->cookies = soup_cookie_jar_get_cookie_list_with_same_site_info (fixture->jar, fixture->origin_uri, + test->cross_origin ? fixture->cross_uri : fixture->origin_uri, + test->cookie_uri_is_origin ? fixture->origin_uri : NULL, + test->unsafe_method ? "POST" : "GET", + test->javascript ? FALSE : TRUE, + test->top_level_nav); + assert_highest_policy_visible (fixture->cookies, test->visible_policy); +} + +int +main (int argc, char **argv) +{ + int ret, i; + SameSiteTest same_site_tests[] = { + /* This does not necessarily cover all combinations since some make no sense in real use */ + + /* Situations where Strict are passed: */ + { .name="/same-site/basic", .visible_policy=SOUP_SAME_SITE_POLICY_STRICT }, + { .name="/same-site/basic-js", .javascript=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT }, + { .name="/same-site/top-level-to-same-site", .top_level_nav=TRUE, .cookie_uri_is_origin=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT }, + { .name="/same-site/top-level-to-same-site-js", .top_level_nav=TRUE, .cookie_uri_is_origin=TRUE, .javascript=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT }, + { .name="/same-site/unsafe-method", .unsafe_method=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT }, + { .name="/same-site/unsafe-method-js", .unsafe_method=TRUE, .javascript=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT }, + { .name="/same-site/cross-top-level-to-same-site", .cross_origin=TRUE, .top_level_nav=TRUE, .cookie_uri_is_origin=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT }, + { .name="/same-site/cross-top-level-to-same-site-js", .cross_origin=TRUE, .javascript=TRUE, .top_level_nav=TRUE, .cookie_uri_is_origin=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_STRICT }, + + /* Situations where Lax are passed: */ + { .name="/same-site/top-level", .top_level_nav=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_LAX }, + { .name="/same-site/top-level-js", .top_level_nav=TRUE, .javascript=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_LAX }, + { .name="/same-site/cross-top-level", .cross_origin=TRUE, .top_level_nav=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_LAX }, + { .name="/same-site/cross-top-level-js", .cross_origin=TRUE, .javascript=TRUE, .top_level_nav=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_LAX }, + { .name="/same-site/cross-unsafe-method-top-level-js", .cross_origin=TRUE, .javascript=TRUE, .unsafe_method=TRUE, .top_level_nav=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_LAX }, + + /* All same-site blocked: */ + { .name="/same-site/cross-basic", .cross_origin=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_NONE }, + { .name="/same-site/cross-basic-js", .cross_origin=TRUE, .javascript=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_NONE }, + { .name="/same-site/cross-unsafe-method", .cross_origin=TRUE, .unsafe_method=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_NONE }, + { .name="/same-site/cross-unsafe-method-js", .cross_origin=TRUE, .javascript=TRUE, .unsafe_method=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_NONE }, + { .name="/same-site/cross-unsafe-method-top-level", .cross_origin=TRUE, .unsafe_method=TRUE, .top_level_nav=TRUE, .visible_policy=SOUP_SAME_SITE_POLICY_NONE }, + }; + + test_init (argc, argv, NULL); + + for (i = 0; i < G_N_ELEMENTS (same_site_tests); ++i) + g_test_add (same_site_tests[i].name, SameSiteFixture, &same_site_tests[i], + same_site_setup, same_site_test, same_site_teardown); + + ret = g_test_run (); + test_cleanup (); + return ret; +} |