diff --git a/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt b/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt index 5c4d1cb87f1d7..5e0667c2dd895 100644 --- a/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt +++ b/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt @@ -8,13 +8,15 @@ $standard = new ReflectionExtension('standard'); var_dump($standard->getClassNames()); ?> --EXPECT-- -array(4) { +array(5) { [0]=> string(22) "__PHP_Incomplete_Class" [1]=> string(14) "AssertionError" [2]=> - string(15) "php_user_filter" + string(8) "SameSite" [3]=> + string(15) "php_user_filter" + [4]=> string(9) "Directory" } diff --git a/ext/session/session.c b/ext/session/session.c index 4bfc973ed537b..40eb3fd579034 100644 --- a/ext/session/session.c +++ b/ext/session/session.c @@ -46,6 +46,7 @@ #include "ext/standard/basic_functions.h" #include "ext/standard/head.h" #include "ext/random/php_random.h" +#include "zend_enum.h" #include "mod_files.h" #include "mod_user.h" @@ -1667,6 +1668,7 @@ PHP_FUNCTION(session_set_cookie_params) zend_string *lifetime = NULL, *path = NULL, *domain = NULL, *samesite = NULL; bool secure = 0, secure_null = 1; bool httponly = 0, httponly_null = 1; + zend_object *same_site_enum = NULL; zend_string *ini_name; zend_result result; int found = 0; @@ -1675,13 +1677,14 @@ PHP_FUNCTION(session_set_cookie_params) return; } - ZEND_PARSE_PARAMETERS_START(1, 5) + ZEND_PARSE_PARAMETERS_START(1, 6) Z_PARAM_ARRAY_HT_OR_LONG(options_ht, lifetime_long) Z_PARAM_OPTIONAL Z_PARAM_STR_OR_NULL(path) Z_PARAM_STR_OR_NULL(domain) Z_PARAM_BOOL_OR_NULL(secure, secure_null) Z_PARAM_BOOL_OR_NULL(httponly, httponly_null) + Z_PARAM_OBJ_OF_CLASS(same_site_enum, SameSite_ce) ZEND_PARSE_PARAMETERS_END(); if (PS(session_status) == php_session_active) { @@ -1717,6 +1720,12 @@ PHP_FUNCTION(session_set_cookie_params) zend_argument_value_error(5, "must be null when argument #1 ($lifetime_or_options) is an array"); RETURN_THROWS(); } + /* Use ArgumentCount error trick similar to base setcookie() function */ + if (same_site_enum) { + zend_argument_count_error("session_set_cookie_params(): Expects exactly 1 arguments when argument #1 " + "($lifetime_or_options) is an array"); + RETURN_THROWS(); + } ZEND_HASH_FOREACH_STR_KEY_VAL(options_ht, key, value) { if (key) { ZVAL_DEREF(value); @@ -1806,6 +1815,20 @@ PHP_FUNCTION(session_set_cookie_params) goto cleanup; } } + if (same_site_enum) { + zval *case_name = zend_enum_fetch_case_name(same_site_enum); + samesite = zend_string_copy(Z_STR_P(case_name)); + + /* Verify that cookie is secure if using SameSite::None, see + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure */ + if (!secure && zend_string_equals_literal(samesite, "None")) { + zend_string_release(samesite); + zend_argument_value_error(6, "can only be SameSite::None if argument #6 ($secure) is true"); + RETVAL_FALSE; + goto cleanup; + /* RETURN_THROWS(); */ + } + } if (samesite) { ini_name = zend_string_init("session.cookie_samesite", sizeof("session.cookie_samesite") - 1, 0); result = zend_alter_ini_entry(ini_name, samesite, PHP_INI_USER, PHP_INI_STAGE_RUNTIME); diff --git a/ext/session/session.stub.php b/ext/session/session.stub.php index bfb6849f45e75..00f0b5d03e878 100644 --- a/ext/session/session.stub.php +++ b/ext/session/session.stub.php @@ -85,7 +85,7 @@ function session_cache_limiter(?string $value = null): string|false {} function session_cache_expire(?int $value = null): int|false {} -function session_set_cookie_params(array|int $lifetime_or_options, ?string $path = null, ?string $domain = null, ?bool $secure = null, ?bool $httponly = null): bool {} +function session_set_cookie_params(array|int $lifetime_or_options, ?string $path = null, ?string $domain = null, ?bool $secure = null, ?bool $httponly = null, SameSite $sameSite = SameSite::Lax): bool {} function session_start(array $options = []): bool {} diff --git a/ext/session/session_arginfo.h b/ext/session/session_arginfo.h index c5d1ae7ba0f0a..6112161149b27 100644 --- a/ext/session/session_arginfo.h +++ b/ext/session/session_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 6bbbdc8c4a33d1ff9984b3d81e4f5c9b76efcb14 */ + * Stub hash: aa1547642579ac7908bc04e837d876e7729e970d */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_session_name, 0, 0, MAY_BE_STRING|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, name, IS_STRING, 1, "null") @@ -83,6 +83,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_session_set_cookie_params, 0, 1, ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, domain, IS_STRING, 1, "null") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, secure, _IS_BOOL, 1, "null") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, httponly, _IS_BOOL, 1, "null") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, sameSite, SameSite, 0, "SameSite::Lax") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_session_start, 0, 0, _IS_BOOL, 0) diff --git a/ext/session/tests/session_set_cookie_params_SameSite_param.phpt b/ext/session/tests/session_set_cookie_params_SameSite_param.phpt new file mode 100644 index 0000000000000..82c616e0501b3 --- /dev/null +++ b/ext/session/tests/session_set_cookie_params_SameSite_param.phpt @@ -0,0 +1,81 @@ +--TEST-- +session_set_cookie_params() SameSite parameter tests +--EXTENSIONS-- +session +--SKIPIF-- + +--FILE-- +getMessage(), \PHP_EOL; +} + +// Check ValueError when using SameSite::None and the cookie is not secure, see +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure +try { + session_set_cookie_params(20, sameSite: SameSite::None); +} catch (\ValueError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + session_set_cookie_params(20, sameSite: SameSite::None, secure: false); +} catch (\ValueError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +// Check get argument count error when using an option array +try { + session_set_cookie_params(['secure' => false], sameSite: SameSite::None); +} catch (\ArgumentCountError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +echo "*** Testing session_set_cookie_params(20, sameSite:) ***\n"; +var_dump(session_set_cookie_params(20, sameSite: SameSite::None, secure: true)); +var_dump(ini_get("session.cookie_samesite")); +var_dump(session_set_cookie_params(20, sameSite: SameSite::Lax, secure: true)); +var_dump(ini_get("session.cookie_samesite")); +var_dump(session_set_cookie_params(20, sameSite: SameSite::Strict, secure: true)); +var_dump(ini_get("session.cookie_samesite")); +// After session started +var_dump(session_start()); +var_dump(session_set_cookie_params(20, sameSite: SameSite::None, secure: true)); +var_dump(ini_get("session.cookie_samesite")); +var_dump(session_set_cookie_params(20, sameSite: SameSite::Lax, secure: true)); +var_dump(ini_get("session.cookie_samesite")); +var_dump(session_set_cookie_params(20, sameSite: SameSite::Strict, secure: true)); + +echo "Done"; +ob_end_flush(); +?> +--EXPECTF-- +session_set_cookie_params(): Argument #6 ($sameSite) must be of type SameSite, stdClass given +session_set_cookie_params(): Argument #6 ($sameSite) can only be SameSite::None if argument #6 ($secure) is true +session_set_cookie_params(): Argument #6 ($sameSite) can only be SameSite::None if argument #6 ($secure) is true +session_set_cookie_params(): Expects exactly 1 arguments when argument #1 ($lifetime_or_options) is an array +*** Testing session_set_cookie_params(20, sameSite:) *** +bool(true) +string(4) "None" +bool(true) +string(3) "Lax" +bool(true) +string(6) "Strict" +bool(true) + +Warning: session_set_cookie_params(): Session cookie parameters cannot be changed when a session is active in %s on line %d +bool(false) +string(6) "Strict" + +Warning: session_set_cookie_params(): Session cookie parameters cannot be changed when a session is active in %s on line %d +bool(false) +string(6) "Strict" + +Warning: session_set_cookie_params(): Session cookie parameters cannot be changed when a session is active in %s on line %d +bool(false) +Done diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 4e10cac38c71b..16af0a9f32bb6 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -32,6 +32,7 @@ #include "php_ext_syslog.h" #include "ext/standard/info.h" #include "ext/session/php_session.h" +#include "zend_enum.h" #include "zend_exceptions.h" #include "zend_attributes.h" #include "zend_ini.h" @@ -306,6 +307,7 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */ php_register_incomplete_class_handlers(); assertion_error_ce = register_class_AssertionError(zend_ce_error); + SameSite_ce = register_class_SameSite(); BASIC_MINIT_SUBMODULE(var) BASIC_MINIT_SUBMODULE(file) diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index ab56a8c0e8fbb..9dddec7db0899 100755 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -2191,9 +2191,15 @@ function header(string $header, bool $replace = true, int $response_code = 0): v function header_remove(?string $name = null): void {} -function setrawcookie(string $name, string $value = "", array|int $expires_or_options = 0, string $path = "", string $domain = "", bool $secure = false, bool $httponly = false): bool {} +enum SameSite { + case None; + case Lax; + case Strict; +} + +function setrawcookie(string $name, string $value = "", array|int $expires_or_options = 0, string $path = "", string $domain = "", bool $secure = false, bool $httponly = false, SameSite $sameSite = SameSite::Lax): bool {} -function setcookie(string $name, string $value = "", array|int $expires_or_options = 0, string $path = "", string $domain = "", bool $secure = false, bool $httponly = false): bool {} +function setcookie(string $name, string $value = "", array|int $expires_or_options = 0, string $path = "", string $domain = "", bool $secure = false, bool $httponly = false, SameSite $sameSite = SameSite::Lax): bool {} function http_response_code(int $response_code = 0): int|bool {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 077f9876df7c2..906d24b88153a 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 39d455982dfdea9d0b9b646bc207b05f7108d1b2 */ + * Stub hash: 817abf90c34d4f39ae209b24f6ff54639e425041 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -760,6 +760,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_setrawcookie, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, domain, IS_STRING, 0, "\"\"") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, secure, _IS_BOOL, 0, "false") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, httponly, _IS_BOOL, 0, "false") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, sameSite, SameSite, 0, "SameSite::Lax") ZEND_END_ARG_INFO() #define arginfo_setcookie arginfo_setrawcookie @@ -3481,6 +3482,11 @@ static const zend_function_entry class_AssertionError_methods[] = { ZEND_FE_END }; + +static const zend_function_entry class_SameSite_methods[] = { + ZEND_FE_END +}; + static void register_basic_functions_symbols(int module_number) { REGISTER_LONG_CONSTANT("EXTR_OVERWRITE", PHP_EXTR_OVERWRITE, CONST_PERSISTENT); @@ -4014,3 +4020,16 @@ static zend_class_entry *register_class_AssertionError(zend_class_entry *class_e return class_entry; } + +static zend_class_entry *register_class_SameSite(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("SameSite", IS_UNDEF, class_SameSite_methods); + + zend_enum_add_case_cstr(class_entry, "None", NULL); + + zend_enum_add_case_cstr(class_entry, "Lax", NULL); + + zend_enum_add_case_cstr(class_entry, "Strict", NULL); + + return class_entry; +} diff --git a/ext/standard/head.c b/ext/standard/head.c index 5bdae98dfce56..2a95f1b3a54ff 100644 --- a/ext/standard/head.c +++ b/ext/standard/head.c @@ -25,7 +25,9 @@ #include "php_globals.h" #include "zend_smart_str.h" +#include "zend_enum.h" +PHPAPI zend_class_entry *SameSite_ce; /* Implementation of the language Header() function */ /* {{{ Sends a raw HTTP header */ @@ -227,9 +229,10 @@ static void php_setcookie_common(INTERNAL_FUNCTION_PARAMETERS, bool is_raw) HashTable *options = NULL; zend_long expires = 0; zend_string *name, *value = NULL, *path = NULL, *domain = NULL, *samesite = NULL; + zend_object *same_site_enum = NULL; bool secure = 0, httponly = 0; - ZEND_PARSE_PARAMETERS_START(1, 7) + ZEND_PARSE_PARAMETERS_START(1, 8) Z_PARAM_STR(name) Z_PARAM_OPTIONAL Z_PARAM_STR(value) @@ -238,6 +241,7 @@ static void php_setcookie_common(INTERNAL_FUNCTION_PARAMETERS, bool is_raw) Z_PARAM_STR(domain) Z_PARAM_BOOL(secure) Z_PARAM_BOOL(httponly) + Z_PARAM_OBJ_OF_CLASS(same_site_enum, SameSite_ce) ZEND_PARSE_PARAMETERS_END(); if (options) { @@ -254,6 +258,19 @@ static void php_setcookie_common(INTERNAL_FUNCTION_PARAMETERS, bool is_raw) } } + if (same_site_enum) { + zval *case_name = zend_enum_fetch_case_name(same_site_enum); + samesite = zend_string_copy(Z_STR_P(case_name)); + + /* Verify that cookie is secure if using SameSite::None, see + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure */ + if (!secure && zend_string_equals_literal(samesite, "None")) { + zend_string_release(samesite); + zend_argument_value_error(8, "can only be SameSite::None if argument #6 ($secure) is true"); + RETURN_THROWS(); + } + } + if (php_setcookie(name, value, expires, path, domain, secure, httponly, samesite, !is_raw) == SUCCESS) { RETVAL_TRUE; } else { diff --git a/ext/standard/head.h b/ext/standard/head.h index a891e05b84eb2..acbc6938e7372 100644 --- a/ext/standard/head.h +++ b/ext/standard/head.h @@ -32,4 +32,6 @@ PHPAPI zend_result php_setcookie(zend_string *name, zend_string *value, time_t e zend_string *path, zend_string *domain, bool secure, bool httponly, zend_string *samesite, bool url_encode); +extern PHPAPI zend_class_entry *SameSite_ce; + #endif diff --git a/ext/standard/tests/network/setcookie.phpt b/ext/standard/tests/network/setcookie.phpt index f43680a5bceae..ab8ef82df4a82 100644 --- a/ext/standard/tests/network/setcookie.phpt +++ b/ext/standard/tests/network/setcookie.phpt @@ -19,6 +19,9 @@ setcookie('name', 'value', 0, '', '', FALSE, TRUE); setcookie('name', 'value', ['expires' => $tsp]); setcookie('name', 'value', ['expires' => $tsn, 'path' => '/path/', 'domain' => 'domain.tld', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict']); +setcookie('name', 'value', /*expires:*/ $tsp, path: '/path/', domain: 'domain.tld', secure: true, httponly: true, sameSite: SameSite::None); +setcookie('name', 'value', /*expires:*/ $tsp, path: '/path/', domain: 'domain.tld', secure: true, httponly: true, sameSite: SameSite::Lax); +setcookie('name', 'value', /*expires:*/ $tsp, path: '/path/', domain: 'domain.tld', secure: true, httponly: true, sameSite: SameSite::Strict); $expected = array( 'Set-Cookie: name=deleted; expires='.date('D, d M Y H:i:s', 1).' GMT; Max-Age=0', @@ -34,7 +37,10 @@ $expected = array( 'Set-Cookie: name=value; secure', 'Set-Cookie: name=value; HttpOnly', 'Set-Cookie: name=value; expires='.date('D, d M Y H:i:s', $tsp).' GMT; Max-Age=5', - 'Set-Cookie: name=value; expires='.date('D, d M Y H:i:s', $tsn).' GMT; Max-Age=0; path=/path/; domain=domain.tld; secure; HttpOnly; SameSite=Strict' + 'Set-Cookie: name=value; expires='.date('D, d M Y H:i:s', $tsn).' GMT; Max-Age=0; path=/path/; domain=domain.tld; secure; HttpOnly; SameSite=Strict', + 'Set-Cookie: name=value; expires='.date('D, d M Y H:i:s', $tsp).' GMT; Max-Age=5; path=/path/; domain=domain.tld; secure; HttpOnly; SameSite=None', + 'Set-Cookie: name=value; expires='.date('D, d M Y H:i:s', $tsp).' GMT; Max-Age=5; path=/path/; domain=domain.tld; secure; HttpOnly; SameSite=Lax', + 'Set-Cookie: name=value; expires='.date('D, d M Y H:i:s', $tsp).' GMT; Max-Age=5; path=/path/; domain=domain.tld; secure; HttpOnly; SameSite=Strict', ); $headers = headers_list(); @@ -44,26 +50,20 @@ if (($i = count($expected)) > count($headers)) return; } -do { - $header = current($headers); - if (strncmp($header, 'Set-Cookie:', 11) !== 0) { +foreach ($headers as $header) { + if (!str_starts_with($header, 'Set-Cookie:')) { continue; } - // If the second rolls over between the time() call and the internal time determination by // setcookie(), we might get Max-Age=4 instead of Max-Age=5. $header = str_replace('Max-Age=4', 'Max-Age=5', $header); if ($header === current($expected)) { $i--; } else { - echo "Header mismatch:\n\tExpected: " - .current($expected) - ."\n\tReceived: ".current($headers)."\n"; + echo "Header mismatch:\n\tExpected: ", current($expected), "\n\tReceived: ", $header, "\n"; } - next($expected); } -while (next($headers) !== FALSE); echo ($i === 0) ? 'OK' diff --git a/ext/standard/tests/network/setcookie_samesite_param.phpt b/ext/standard/tests/network/setcookie_samesite_param.phpt new file mode 100644 index 0000000000000..d612ed9722d03 --- /dev/null +++ b/ext/standard/tests/network/setcookie_samesite_param.phpt @@ -0,0 +1,33 @@ +--TEST-- +setcookie() SameSite parameter errors +--FILE-- +getMessage(), \PHP_EOL; +} + +// Check ValueError when using SameSite::None and the cookie is not secure, see +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure +try { + setcookie('name', sameSite: SameSite::None); +} catch (\ValueError $e) { + echo $e->getMessage(), \PHP_EOL; +} +try { + setcookie('name', sameSite: SameSite::None, secure: false); +} catch (\ValueError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +// Sanity enum check +var_dump(SameSite::Strict); +?> +--EXPECT-- +setcookie(): Argument #8 ($sameSite) must be of type SameSite, stdClass given +setcookie(): Argument #8 ($sameSite) can only be SameSite::None if argument #6 ($secure) is true +setcookie(): Argument #8 ($sameSite) can only be SameSite::None if argument #6 ($secure) is true +enum(SameSite::Strict)