Skip to content
Open
21 changes: 19 additions & 2 deletions src/wp-activate.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,18 @@
$redirect_url = remove_query_arg( 'key' );

if ( remove_query_arg( false ) !== $redirect_url ) {
setcookie( $activate_cookie, $key, 0, $activate_path, COOKIE_DOMAIN, is_ssl(), true );
wp_set_cookie(
$activate_cookie,
$key,
array(
'expires' => 0,
'path' => $activate_path,
'domain' => COOKIE_DOMAIN,
'secure' => is_ssl(),
'httponly' => true,
'samesite' => 'Lax',
)
);
wp_safe_redirect( $redirect_url );
exit;
} else {
Expand All @@ -49,7 +60,13 @@
if ( null === $result && isset( $_COOKIE[ $activate_cookie ] ) ) {
$key = $_COOKIE[ $activate_cookie ];
$result = wpmu_activate_signup( $key );
setcookie( $activate_cookie, ' ', time() - YEAR_IN_SECONDS, $activate_path, COOKIE_DOMAIN, is_ssl(), true );
wp_unset_cookie(
$activate_cookie,
array(
'path' => $activate_path,
'domain' => COOKIE_DOMAIN,
)
);
}

if ( null === $result || ( is_wp_error( $result ) && 'invalid_key' === $result->get_error_code() ) ) {
Expand Down
13 changes: 12 additions & 1 deletion src/wp-admin/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,18 @@

// Session cookie flag that the post was saved.
if ( isset( $_COOKIE['wp-saving-post'] ) && $_COOKIE['wp-saving-post'] === $post_id . '-check' ) {
setcookie( 'wp-saving-post', $post_id . '-saved', time() + DAY_IN_SECONDS, ADMIN_COOKIE_PATH, COOKIE_DOMAIN, is_ssl() );
wp_set_cookie(
'wp-saving-post',
$post_id . '-saved',
array(
'expires' => time() + DAY_IN_SECONDS,
'path' => ADMIN_COOKIE_PATH,
'domain' => COOKIE_DOMAIN,
'secure' => is_ssl(),
'httponly' => false,
'samesite' => 'Lax',
)
);
}

redirect_post( $post_id ); // Send user on their way while we keep working.
Expand Down
42 changes: 38 additions & 4 deletions src/wp-includes/class-wp-recovery-mode-cookie-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,32 @@ public function set_cookie() {

$expire = time() + $length;

setcookie( RECOVERY_MODE_COOKIE, $value, $expire, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
wp_set_cookie(
RECOVERY_MODE_COOKIE,
$value,
array(
'expires' => $expire,
'path' => COOKIEPATH,
'domain' => COOKIE_DOMAIN,
'secure' => is_ssl(),
'httponly' => true,
'samesite' => 'Lax',
)
);

if ( COOKIEPATH !== SITECOOKIEPATH ) {
setcookie( RECOVERY_MODE_COOKIE, $value, $expire, SITECOOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
wp_set_cookie(
RECOVERY_MODE_COOKIE,
$value,
array(
'expires' => $expire,
'path' => SITECOOKIEPATH,
'domain' => COOKIE_DOMAIN,
'secure' => is_ssl(),
'httponly' => true,
'samesite' => 'Lax',
)
);
}
}

Expand All @@ -60,8 +82,20 @@ public function set_cookie() {
* @since 5.2.0
*/
public function clear_cookie() {
setcookie( RECOVERY_MODE_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
setcookie( RECOVERY_MODE_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN );
wp_unset_cookie(
RECOVERY_MODE_COOKIE,
array(
'path' => COOKIEPATH,
'domain' => COOKIE_DOMAIN,
)
);
wp_unset_cookie(
RECOVERY_MODE_COOKIE,
array(
'path' => SITECOOKIEPATH,
'domain' => COOKIE_DOMAIN,
)
);
}

/**
Expand Down
64 changes: 57 additions & 7 deletions src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -586,10 +586,27 @@ function wp_set_comment_cookies( $comment, $user, $cookies_consent = true ) {

if ( false === $cookies_consent ) {
// Remove any existing cookies.
$past = time() - YEAR_IN_SECONDS;
setcookie( 'comment_author_' . COOKIEHASH, ' ', $past, COOKIEPATH, COOKIE_DOMAIN );
setcookie( 'comment_author_email_' . COOKIEHASH, ' ', $past, COOKIEPATH, COOKIE_DOMAIN );
setcookie( 'comment_author_url_' . COOKIEHASH, ' ', $past, COOKIEPATH, COOKIE_DOMAIN );
wp_unset_cookie(
'comment_author_' . COOKIEHASH,
array(
'path' => COOKIEPATH,
'domain' => COOKIE_DOMAIN,
)
);
wp_unset_cookie(
'comment_author_email_' . COOKIEHASH,
array(
'path' => COOKIEPATH,
'domain' => COOKIE_DOMAIN,
)
);
wp_unset_cookie(
'comment_author_url_' . COOKIEHASH,
array(
'path' => COOKIEPATH,
'domain' => COOKIE_DOMAIN,
)
);

return;
}
Expand All @@ -606,9 +623,42 @@ function wp_set_comment_cookies( $comment, $user, $cookies_consent = true ) {

$secure = ( 'https' === parse_url( home_url(), PHP_URL_SCHEME ) );

setcookie( 'comment_author_' . COOKIEHASH, $comment->comment_author, $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN, $secure );
setcookie( 'comment_author_email_' . COOKIEHASH, $comment->comment_author_email, $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN, $secure );
setcookie( 'comment_author_url_' . COOKIEHASH, esc_url( $comment->comment_author_url ), $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN, $secure );
wp_set_cookie(
'comment_author_' . COOKIEHASH,
$comment->comment_author,
array(
'expires' => $comment_cookie_lifetime,
'path' => COOKIEPATH,
'domain' => COOKIE_DOMAIN,
'secure' => $secure,
'httponly' => false,
'samesite' => 'Lax',
)
);
wp_set_cookie(
'comment_author_email_' . COOKIEHASH,
$comment->comment_author_email,
array(
'expires' => $comment_cookie_lifetime,
'path' => COOKIEPATH,
'domain' => COOKIE_DOMAIN,
'secure' => $secure,
'httponly' => false,
'samesite' => 'Lax',
)
);
wp_set_cookie(
'comment_author_url_' . COOKIEHASH,
esc_url( $comment->comment_author_url ),
array(
'expires' => $comment_cookie_lifetime,
'path' => COOKIEPATH,
'domain' => COOKIE_DOMAIN,
'secure' => $secure,
'httponly' => false,
'samesite' => 'Lax',
)
);
}

/**
Expand Down
93 changes: 93 additions & 0 deletions src/wp-includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,99 @@ function nocache_headers() {
}
}

/**
* Defines a cookie to be sent along with the rest of the HTTP headers.
*
* Wrapper for PHP's native setcookie() that provides a filter to adjust the
* options for all cookies in one place, and a short-circuit filter to prevent
* a cookie from being sent.
*
* The options are passed to setcookie() unchanged, so its native defaults apply
* to any that are omitted.
*
* @since 7.1.0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These @since 7.1.0 tags — this one, the two hook docblocks at 1601 and 1614, and wp_unset_cookie() at 1635 — should be set from the release schedule at commit time rather than from this branch.

Flagging it because it currently reads correct in context and is easy to skim past: this branch's version.php is still 7.1-alpha-62161-src, while trunk has moved on to 7.2-alpha. The Trac milestone says 7.2, and I've heard 7.1.1 mentioned — whichever it lands in, all four need to match.

One wrinkle if it does go to 7.1.1: this adds two public functions and two public hooks, which is more new API surface than a point release normally carries.

*
* @param string $name The name of the cookie.
* @param string $value The value of the cookie.
* @param array $options {
* Optional. Options to pass to setcookie(). Default empty array.
*
* @type int $expires The time the cookie expires, as a Unix timestamp.
* @type string $path The path on the server in which the cookie will be available on.
* @type string $domain The (sub)domain that the cookie is available to.
* @type bool $secure Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client.
* @type bool $httponly When true the cookie will be made accessible only through the HTTP protocol.
* @type string $samesite Whether the cookie should be available for cross-site requests. Accepts 'Lax', 'Strict', or 'None'.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the claim is that this accepts these values, but we never verify that. perhaps it would be worth adding a check and a _doing_it_wrong(), given the sensitive nature of this flag.

additionally, are we setting the default values anywhere? what if someone passes the options but omits the samesite property? perhaps we would want to use latching logic instead on some of these properties.

something like this…

$options = filter…
$options = filter…

…

$options['samesite'] = isset( $options['samesite'] ) && in_array( $options['samesite'], array( 'Lax', 'Strict', 'None' ), true )
	? $options['samesite']
	: 'Lax;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the claim is that this accepts these values, but we never verify that. perhaps it would be worth adding a check and a _doing_it_wrong(), given the sensitive nature of this flag.

Good point. Apparently PHP doesn't do any validation here either, passing through strings verbatim. At least there is PHPStan typing added to help guard against this, but it is case-sensitive. We could do case-insensitive checking and raise _doing_it_wrong() if there is no match.

Something that Claude pointed out is that SameSite=None doesn't do anything unless Secure is also set. So perhaps _doing_it_wrong() should be raised if SameSite is being set but Secure isn't, cf. MDN:

Send the cookie with both cross-site and same-site requests. The Secure attribute must also be set when using this value.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems there would be, at a minimum, a warrant to perform consistency and coherency checks here. I saw something else too about Chromium treating the absence of SameSite as being distinct from SameSite=LaxLax-allowing-unsafe. I don’t know if that’s true, but it’s a claim that the distinction matters to avoid breaking SAML/SSO flows, payment redirects, and reliance on cookies generated immediately before a cross-site POST

* }
* @return bool Whether the cookie was sent successfully.
* @phpstan-param array{
* expires?: int,
* path?: string,
* domain?: string,
* secure?: bool,
* httponly?: bool,
* samesite?: 'Lax'|'Strict'|'None',
* } $options
*/
function wp_set_cookie( string $name, string $value, array $options = array() ): bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should wpCookies.set() in JavaScript also be updated to avoid inconsistencies between JS fetches and HTML-originated browser fetches? or does the policy carry over?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to @nimesh-xecurify's suggestion on defaulting samesite here — there's supporting evidence inside this PR itself.

Counting the call sites the diff adds: all 19 wp_set_cookie() calls pass 'samesite' => 'Lax', and all 26 wp_unset_cookie() calls omit it, including the auth-cookie clearing in wp_clear_auth_cookie(). Nothing is broken by that — browsers match a deletion on name/path/domain, not on SameSite — but it does mean the convention the patch relies on is already only half-followed by the patch.

$options = wp_parse_args( $options, array( 'samesite' => 'Lax' ) );

before the wp_set_cookie_options filter would cover every caller, current and future, including plugins that adopt the wrapper — and would let the explicit 'samesite' => 'Lax' come back out of the 19 call sites.

/**
* Filters the options used when a cookie is sent to the browser.
*
* @since 7.1.0
*
* @param array $options The options to pass to setcookie().
* @param string $name The name of the cookie.
* @param string $value The value of the cookie.
*/
$options = apply_filters( 'wp_set_cookie_options', $options, $name, $value );

/**
* Filters whether a cookie should be sent to the browser.
*
* Returning false prevents the cookie from being sent.
*
* @since 7.1.0
*
* @param bool $send Whether to send the cookie. Default true.
* @param string $name The name of the cookie.
* @param string $value The value of the cookie.
* @param array $options The options to pass to setcookie().
*/
if ( ! apply_filters( 'send_cookie', true, $name, $value, $options ) ) {
return false;
}
Comment on lines +1621 to +1623

return setcookie( $name, $value, $options );
}

/**
* Removes a cookie from the browser.
*
* Sends a cookie with an empty value and an expiry time in the past, which
* instructs the browser to delete it. The path and domain must match those
* used when the cookie was originally set for the removal to take effect.
*
* @since 7.1.0
*
* @param string $name The name of the cookie.
* @param array $options Optional. Options to pass to setcookie(). See {@see wp_set_cookie()} for the full list.
* Default empty array.
* @return bool True if the cookie was removed successfully, false otherwise.
* @phpstan-param array{
* expires?: int,
* path?: string,
* domain?: string,
* secure?: bool,
* httponly?: bool,
* samesite?: 'Lax'|'Strict'|'None',
* } $options
*/
function wp_unset_cookie( string $name, array $options = array() ): bool {
$options['expires'] = time() - YEAR_IN_SECONDS;

return wp_set_cookie( $name, ' ', $options );
}

/**
* Sets the HTTP headers for caching for 10 days with JavaScript content type.
*
Expand Down
33 changes: 30 additions & 3 deletions src/wp-includes/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -1737,8 +1737,30 @@ function wp_user_settings() {

// The cookie is not set in the current browser or the saved value is newer.
$secure = ( 'https' === parse_url( admin_url(), PHP_URL_SCHEME ) );
setcookie( 'wp-settings-' . $user_id, $settings, time() + YEAR_IN_SECONDS, SITECOOKIEPATH, '', $secure );
setcookie( 'wp-settings-time-' . $user_id, time(), time() + YEAR_IN_SECONDS, SITECOOKIEPATH, '', $secure );
wp_set_cookie(
'wp-settings-' . $user_id,
$settings,
array(
'expires' => time() + YEAR_IN_SECONDS,
'path' => SITECOOKIEPATH,
'domain' => '',
'secure' => $secure,
'httponly' => false,
'samesite' => 'Lax',
)
);
wp_set_cookie(
'wp-settings-time-' . $user_id,
(string) time(),
array(
'expires' => time() + YEAR_IN_SECONDS,
'path' => SITECOOKIEPATH,
'domain' => '',
'secure' => $secure,
'httponly' => false,
'samesite' => 'Lax',
)
);
$_COOKIE[ 'wp-settings-' . $user_id ] = $settings;
}

Expand Down Expand Up @@ -1914,7 +1936,12 @@ function delete_all_user_settings() {
}

update_user_option( $user_id, 'user-settings', '', false );
setcookie( 'wp-settings-' . $user_id, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH );
wp_unset_cookie(
'wp-settings-' . $user_id,
array(
'path' => SITECOOKIEPATH,
)
);
}

/**
Expand Down
Loading
Loading