From 487e1e1e5954a71695fdb9dbcad39e8843c5f20c Mon Sep 17 00:00:00 2001 From: Frederik Bosch Date: Fri, 7 Jul 2017 19:06:53 +0200 Subject: [PATCH 1/2] implement same site cookie see https://bugs.php.net/bug.php?id=72230 see https://tools.ietf.org/html/draft-west-first-party-cookies-07 see https://scotthelme.co.uk/csrf-is-dead/ --- ext/session/php_session.h | 1 + ext/session/session.c | 20 ++++++-- .../session_get_cookie_params_basic.phpt | 17 +++++-- .../session_get_cookie_params_variation1.phpt | 41 ++++++++++++--- .../session_set_cookie_params_variation3.phpt | 2 +- .../session_set_cookie_params_variation5.phpt | 2 +- .../session_set_cookie_params_variation6.phpt | 50 +++++++++++++++++++ ext/standard/basic_functions.c | 2 + ext/standard/head.c | 28 +++++++---- ext/standard/head.h | 3 +- php.ini-development | 5 ++ php.ini-production | 5 ++ 12 files changed, 149 insertions(+), 27 deletions(-) create mode 100644 ext/session/tests/session_set_cookie_params_variation6.phpt diff --git a/ext/session/php_session.h b/ext/session/php_session.h index a44cac77c76da..4f8ddc812651a 100644 --- a/ext/session/php_session.h +++ b/ext/session/php_session.h @@ -157,6 +157,7 @@ typedef struct _php_ps_globals { char *cookie_domain; zend_bool cookie_secure; zend_bool cookie_httponly; + char *cookie_samesite; const ps_module *mod; const ps_module *default_mod; void *mod_data; diff --git a/ext/session/session.c b/ext/session/session.c index a86f7c25de284..799d1b480589d 100644 --- a/ext/session/session.c +++ b/ext/session/session.c @@ -804,6 +804,7 @@ PHP_INI_BEGIN() STD_PHP_INI_ENTRY("session.cookie_domain", "", PHP_INI_ALL, OnUpdateSessionString, cookie_domain, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.cookie_secure", "0", PHP_INI_ALL, OnUpdateSessionBool, cookie_secure, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.cookie_httponly", "0", PHP_INI_ALL, OnUpdateSessionBool, cookie_httponly, php_ps_globals, ps_globals) + STD_PHP_INI_ENTRY("session.cookie_samesite", "", PHP_INI_ALL, OnUpdateString, cookie_samesite, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.use_cookies", "1", PHP_INI_ALL, OnUpdateSessionBool, use_cookies, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.use_only_cookies", "1", PHP_INI_ALL, OnUpdateSessionBool, use_only_cookies, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.use_strict_mode", "0", PHP_INI_ALL, OnUpdateSessionBool, use_strict_mode, php_ps_globals, ps_globals) @@ -1362,6 +1363,11 @@ static int php_session_send_cookie(void) /* {{{ */ smart_str_appends(&ncookie, COOKIE_HTTPONLY); } + if (PS(cookie_samesite)[0]) { + smart_str_appends(&ncookie, COOKIE_SAMESITE); + smart_str_appends(&ncookie, PS(cookie_samesite)); + } + smart_str_0(&ncookie); php_session_remove_cookie(); /* remove already sent session ID cookie */ @@ -1660,18 +1666,18 @@ PHPAPI void session_adapt_url(const char *url, size_t urllen, char **new, size_t * Userspace exported functions * ******************************** */ -/* {{{ proto bool session_set_cookie_params(int lifetime [, string path [, string domain [, bool secure[, bool httponly]]]]) +/* {{{ proto void session_set_cookie_params(int lifetime [, string path [, string domain [, bool secure[, bool httponly[, string samesite]]]]]) Set session cookie parameters */ static PHP_FUNCTION(session_set_cookie_params) { zval *lifetime; - zend_string *path = NULL, *domain = NULL; + zend_string *path = NULL, *domain = NULL, *samesite = NULL; int argc = ZEND_NUM_ARGS(); zend_bool secure = 0, httponly = 0; zend_string *ini_name; if (!PS(use_cookies) || - zend_parse_parameters(argc, "z|SSbb", &lifetime, &path, &domain, &secure, &httponly) == FAILURE) { + zend_parse_parameters(argc, "z|SSbbS", &lifetime, &path, &domain, &secure, &httponly, &samesite) == FAILURE) { return; } @@ -1729,6 +1735,12 @@ static PHP_FUNCTION(session_set_cookie_params) zend_string_release_ex(ini_name, 0); } + if (argc > 5) { + ini_name = zend_string_init("session.cookie_samesite", sizeof("session.cookie_samesite") - 1, 0); + zend_alter_ini_entry(ini_name, samesite, PHP_INI_USER, PHP_INI_STAGE_RUNTIME); + zend_string_release(ini_name); + } + RETURN_TRUE; } /* }}} */ @@ -1748,6 +1760,7 @@ static PHP_FUNCTION(session_get_cookie_params) add_assoc_string(return_value, "domain", PS(cookie_domain)); add_assoc_bool(return_value, "secure", PS(cookie_secure)); add_assoc_bool(return_value, "httponly", PS(cookie_httponly)); + add_assoc_string(return_value, "samesite", PS(cookie_samesite)); } /* }}} */ @@ -2632,6 +2645,7 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_session_set_cookie_params, 0, 0, 1) ZEND_ARG_INFO(0, domain) ZEND_ARG_INFO(0, secure) ZEND_ARG_INFO(0, httponly) + ZEND_ARG_INFO(0, samesite) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_session_class_open, 0) diff --git a/ext/session/tests/session_get_cookie_params_basic.phpt b/ext/session/tests/session_get_cookie_params_basic.phpt index ca4d330e677e3..9dcde4ec350b2 100644 --- a/ext/session/tests/session_get_cookie_params_basic.phpt +++ b/ext/session/tests/session_get_cookie_params_basic.phpt @@ -8,6 +8,7 @@ session.cookie_path="/" session.cookie_domain="" session.cookie_secure=0 session.cookie_httponly=0 +session.cookie_samesite="" --FILE-- --EXPECT-- *** Testing session_get_cookie_params() : basic functionality *** -array(5) { +array(6) { ["lifetime"]=> int(0) ["path"]=> @@ -43,9 +44,11 @@ array(5) { bool(false) ["httponly"]=> bool(false) + ["samesite"]=> + string(0) "" } bool(true) -array(5) { +array(6) { ["lifetime"]=> int(3600) ["path"]=> @@ -56,9 +59,11 @@ array(5) { bool(false) ["httponly"]=> bool(false) + ["samesite"]=> + string(3) "foo" } bool(true) -array(5) { +array(6) { ["lifetime"]=> int(1234567890) ["path"]=> @@ -69,5 +74,7 @@ array(5) { bool(true) ["httponly"]=> bool(true) + ["samesite"]=> + string(4) "blah" } Done diff --git a/ext/session/tests/session_get_cookie_params_variation1.phpt b/ext/session/tests/session_get_cookie_params_variation1.phpt index 6d5f816cdea8b..f7fd60deae0b0 100644 --- a/ext/session/tests/session_get_cookie_params_variation1.phpt +++ b/ext/session/tests/session_get_cookie_params_variation1.phpt @@ -8,6 +8,7 @@ session.cookie_path="/" session.cookie_domain="" session.cookie_secure=0 session.cookie_httponly=0 +session.cookie_samesite="" --FILE-- --EXPECT-- *** Testing session_get_cookie_params() : variation *** -array(5) { +array(6) { ["lifetime"]=> int(0) ["path"]=> @@ -49,8 +52,10 @@ array(5) { bool(false) ["httponly"]=> bool(false) + ["samesite"]=> + string(0) "" } -array(5) { +array(6) { ["lifetime"]=> int(3600) ["path"]=> @@ -61,8 +66,10 @@ array(5) { bool(false) ["httponly"]=> bool(false) + ["samesite"]=> + string(0) "" } -array(5) { +array(6) { ["lifetime"]=> int(3600) ["path"]=> @@ -73,8 +80,10 @@ array(5) { bool(false) ["httponly"]=> bool(false) + ["samesite"]=> + string(0) "" } -array(5) { +array(6) { ["lifetime"]=> int(3600) ["path"]=> @@ -85,8 +94,10 @@ array(5) { bool(false) ["httponly"]=> bool(false) + ["samesite"]=> + string(0) "" } -array(5) { +array(6) { ["lifetime"]=> int(3600) ["path"]=> @@ -97,8 +108,10 @@ array(5) { bool(true) ["httponly"]=> bool(false) + ["samesite"]=> + string(0) "" } -array(5) { +array(6) { ["lifetime"]=> int(3600) ["path"]=> @@ -109,6 +122,22 @@ array(5) { bool(true) ["httponly"]=> bool(true) + ["samesite"]=> + string(0) "" +} +array(6) { + ["lifetime"]=> + int(3600) + ["path"]=> + string(5) "/path" + ["domain"]=> + string(3) "foo" + ["secure"]=> + bool(true) + ["httponly"]=> + bool(true) + ["samesite"]=> + string(3) "foo" } Done diff --git a/ext/session/tests/session_set_cookie_params_variation3.phpt b/ext/session/tests/session_set_cookie_params_variation3.phpt index 17d1e6a7713d0..e4e26b91b2ace 100644 --- a/ext/session/tests/session_set_cookie_params_variation3.phpt +++ b/ext/session/tests/session_set_cookie_params_variation3.phpt @@ -10,7 +10,7 @@ session.cookie_domain=foo ob_start(); /* - * Prototype : void session_set_cookie_params(int $lifetime [, string $path [, string $domain [, bool $secure [, bool $httponly]]]]) + * Prototype : void session_set_cookie_params(int $lifetime [, string $path [, string $domain [, bool $secure [, bool $httponly[, string $samesite]]]]]) * Description : Set the session cookie parameters * Source code : ext/session/session.c */ diff --git a/ext/session/tests/session_set_cookie_params_variation5.phpt b/ext/session/tests/session_set_cookie_params_variation5.phpt index ffdd29db2d48c..f3374d0daaad1 100644 --- a/ext/session/tests/session_set_cookie_params_variation5.phpt +++ b/ext/session/tests/session_set_cookie_params_variation5.phpt @@ -10,7 +10,7 @@ session.cookie_httponly=TRUE ob_start(); /* - * Prototype : void session_set_cookie_params(int $lifetime [, string $path [, string $domain [, bool $secure [, bool $httponly]]]]) + * Prototype : void session_set_cookie_params(int $lifetime [, string $path [, string $domain [, bool $secure [, bool $httponly[, string $samesite]]]]]) * Description : Set the session cookie parameters * Source code : ext/session/session.c */ diff --git a/ext/session/tests/session_set_cookie_params_variation6.phpt b/ext/session/tests/session_set_cookie_params_variation6.phpt new file mode 100644 index 0000000000000..7fe1e009eea6d --- /dev/null +++ b/ext/session/tests/session_set_cookie_params_variation6.phpt @@ -0,0 +1,50 @@ +--TEST-- +Test session_set_cookie_params() function : variation +--INI-- +session.cookie_samesite=test +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +*** Testing session_set_cookie_params() : variation *** +string(4) "test" +bool(true) +string(7) "nothing" +bool(true) +string(7) "nothing" + +Warning: session_set_cookie_params(): Cannot change session cookie parameters when session is active in %s on line 18 +bool(false) +string(7) "nothing" +bool(true) +string(7) "nothing" +bool(true) +string(5) "other" +Done diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 85092dea10381..6849014122996 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -1438,6 +1438,7 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_setcookie, 0, 0, 1) ZEND_ARG_INFO(0, domain) ZEND_ARG_INFO(0, secure) ZEND_ARG_INFO(0, httponly) + ZEND_ARG_INFO(0, samesite) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_setrawcookie, 0, 0, 1) @@ -1448,6 +1449,7 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_setrawcookie, 0, 0, 1) ZEND_ARG_INFO(0, domain) ZEND_ARG_INFO(0, secure) ZEND_ARG_INFO(0, httponly) + ZEND_ARG_INFO(0, samesite) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_headers_sent, 0, 0, 0) diff --git a/ext/standard/head.c b/ext/standard/head.c index 1ddbc187a0f4b..487cc648eb9c1 100644 --- a/ext/standard/head.c +++ b/ext/standard/head.c @@ -80,8 +80,7 @@ PHPAPI int php_header(void) } } - -PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, zend_string *path, zend_string *domain, int secure, int url_encode, int httponly) +PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, zend_string *path, zend_string *domain, int secure, int url_encode, int httponly, zend_string *samesite) { char *cookie; size_t len = sizeof("Set-Cookie: "); @@ -131,6 +130,9 @@ PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, if (domain) { len += ZSTR_LEN(domain); } + if (samesite) { + len += ZSTR_LEN(samesite); + } cookie = emalloc(len + 100); @@ -192,6 +194,10 @@ PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, if (httponly) { strlcat(cookie, COOKIE_HTTPONLY, len + 100); } + if (samesite && ZSTR_LEN(samesite)) { + strlcat(cookie, COOKIE_SAMESITE, len + 100); + strlcat(cookie, ZSTR_VAL(samesite), len + 100); + } ctr.line = cookie; ctr.line_len = (uint32_t)strlen(cookie); @@ -203,15 +209,15 @@ PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, /* php_set_cookie(name, value, expires, path, domain, secure) */ -/* {{{ proto bool setcookie(string name [, string value [, int expires [, string path [, string domain [, bool secure[, bool httponly]]]]]]) +/* {{{ proto bool setcookie(string name [, string value [, int expires [, string path [, string domain [, bool secure[, bool httponly[, string samesite]]]]]]]) Send a cookie */ PHP_FUNCTION(setcookie) { - zend_string *name, *value = NULL, *path = NULL, *domain = NULL; + zend_string *name, *value = NULL, *path = NULL, *domain = NULL, *samesite = NULL; zend_long expires = 0; zend_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) @@ -220,9 +226,10 @@ PHP_FUNCTION(setcookie) Z_PARAM_STR(domain) Z_PARAM_BOOL(secure) Z_PARAM_BOOL(httponly) + Z_PARAM_STR(samesite) ZEND_PARSE_PARAMETERS_END(); - if (php_setcookie(name, value, expires, path, domain, secure, 1, httponly) == SUCCESS) { + if (php_setcookie(name, value, expires, path, domain, secure, 1, httponly, samesite) == SUCCESS) { RETVAL_TRUE; } else { RETVAL_FALSE; @@ -230,15 +237,15 @@ PHP_FUNCTION(setcookie) } /* }}} */ -/* {{{ proto bool setrawcookie(string name [, string value [, int expires [, string path [, string domain [, bool secure[, bool httponly]]]]]]) +/* {{{ proto bool setrawcookie(string name [, string value [, int expires [, string path [, string domain [, bool secure[, bool httponly[, string samesite]]]]]]]) Send a cookie with no url encoding of the value */ PHP_FUNCTION(setrawcookie) { - zend_string *name, *value = NULL, *path = NULL, *domain = NULL; + zend_string *name, *value = NULL, *path = NULL, *domain = NULL, *samesite = NULL; zend_long expires = 0; zend_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) @@ -247,9 +254,10 @@ PHP_FUNCTION(setrawcookie) Z_PARAM_STR(domain) Z_PARAM_BOOL(secure) Z_PARAM_BOOL(httponly) + Z_PARAM_STR(samesite) ZEND_PARSE_PARAMETERS_END(); - if (php_setcookie(name, value, expires, path, domain, secure, 0, httponly) == SUCCESS) { + if (php_setcookie(name, value, expires, path, domain, secure, 0, httponly, samesite) == SUCCESS) { RETVAL_TRUE; } else { RETVAL_FALSE; diff --git a/ext/standard/head.h b/ext/standard/head.h index 4a9e291364527..17d15bf2ca4f0 100644 --- a/ext/standard/head.h +++ b/ext/standard/head.h @@ -27,6 +27,7 @@ #define COOKIE_PATH "; path=" #define COOKIE_SECURE "; secure" #define COOKIE_HTTPONLY "; HttpOnly" +#define COOKIE_SAMESITE "; SameSite=" extern PHP_RINIT_FUNCTION(head); PHP_FUNCTION(header); @@ -38,6 +39,6 @@ PHP_FUNCTION(headers_list); PHP_FUNCTION(http_response_code); PHPAPI int php_header(void); -PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, zend_string *path, zend_string *domain, int secure, int url_encode, int httponly); +PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, zend_string *path, zend_string *domain, int secure, int url_encode, int httponly, zend_string *samesite); #endif diff --git a/php.ini-development b/php.ini-development index 3f45c084985e2..1f2cf82d684d2 100644 --- a/php.ini-development +++ b/php.ini-development @@ -1380,6 +1380,11 @@ session.cookie_domain = ; http://php.net/session.cookie-httponly session.cookie_httponly = +; Add SameSite attribute to cookie to help mitigate Cross-Site Request Forgery (CSRF/XSRF) +; Current valid values are "Lax" or "Strict" +; https://tools.ietf.org/html/draft-west-first-party-cookies-07 +session.cookie_samesite = + ; Handler used to serialize data. php is the standard serializer of PHP. ; http://php.net/session.serialize-handler session.serialize_handler = php diff --git a/php.ini-production b/php.ini-production index 793174d88fbcb..5f43f8676fdbd 100644 --- a/php.ini-production +++ b/php.ini-production @@ -1387,6 +1387,11 @@ session.cookie_domain = ; http://php.net/session.cookie-httponly session.cookie_httponly = +; Add SameSite attribute to cookie to help mitigate Cross-Site Request Forgery (CSRF/XSRF) +; Current valid values are "Lax" or "Strict" +; https://tools.ietf.org/html/draft-west-first-party-cookies-07 +session.cookie_samesite = + ; Handler used to serialize data. php is the standard serializer of PHP. ; http://php.net/session.serialize-handler session.serialize_handler = php From f8b6c8a5c8c68086011b456b68ad8616ca86908f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Magalh=C3=A3es?= Date: Thu, 19 Jul 2018 02:40:39 +0100 Subject: [PATCH 2/2] Support for samesite cookies with array syntax Allows using an alternative array argument with support for the samesite option on the following functions: setcookie setrawcookie session_set_cookie_params --- ext/session/session.c | 132 +++++++++++++----- .../session_get_cookie_params_basic.phpt | 31 +++- .../session_set_cookie_params_variation3.phpt | 2 +- .../session_set_cookie_params_variation5.phpt | 2 +- .../session_set_cookie_params_variation6.phpt | 8 +- .../session_set_cookie_params_variation7.phpt | 60 ++++++++ ext/standard/basic_functions.c | 6 +- ext/standard/head.c | 121 ++++++++++++++-- ext/standard/head.h | 2 +- ext/standard/tests/network/setcookie.phpt | 6 +- .../tests/network/setcookie_error.phpt | 26 ++++ 11 files changed, 331 insertions(+), 65 deletions(-) create mode 100644 ext/session/tests/session_set_cookie_params_variation7.phpt create mode 100644 ext/standard/tests/network/setcookie_error.phpt diff --git a/ext/session/session.c b/ext/session/session.c index 799d1b480589d..1420556d29410 100644 --- a/ext/session/session.c +++ b/ext/session/session.c @@ -1666,21 +1666,31 @@ PHPAPI void session_adapt_url(const char *url, size_t urllen, char **new, size_t * Userspace exported functions * ******************************** */ -/* {{{ proto void session_set_cookie_params(int lifetime [, string path [, string domain [, bool secure[, bool httponly[, string samesite]]]]]) +/* {{{ proto bool session_set_cookie_params(int lifetime [, string path [, string domain [, bool secure[, bool httponly]]]]) + session_set_cookie_params(array options) Set session cookie parameters */ static PHP_FUNCTION(session_set_cookie_params) { - zval *lifetime; - zend_string *path = NULL, *domain = NULL, *samesite = NULL; - int argc = ZEND_NUM_ARGS(); - zend_bool secure = 0, httponly = 0; + zval *lifetime_or_options = NULL; + zend_string *lifetime = NULL, *path = NULL, *domain = NULL, *samesite = NULL; + zend_bool secure = 0, secure_null = 1; + zend_bool httponly = 0, httponly_null = 1; zend_string *ini_name; + int result; + int found = 0; - if (!PS(use_cookies) || - zend_parse_parameters(argc, "z|SSbbS", &lifetime, &path, &domain, &secure, &httponly, &samesite) == FAILURE) { + if (!PS(use_cookies)) { return; } + ZEND_PARSE_PARAMETERS_START(1, 5) + Z_PARAM_ZVAL(lifetime_or_options) + Z_PARAM_OPTIONAL + Z_PARAM_STR(path) + Z_PARAM_STR(domain) + Z_PARAM_BOOL_EX(secure, secure_null, 1, 0) + Z_PARAM_BOOL_EX(httponly, httponly_null, 1, 0) + ZEND_PARSE_PARAMETERS_END(); if (PS(session_status) == php_session_active) { php_error_docref(NULL, E_WARNING, "Cannot change session cookie parameters when session is active"); @@ -1692,55 +1702,108 @@ static PHP_FUNCTION(session_set_cookie_params) RETURN_FALSE; } - convert_to_string_ex(lifetime); + if (Z_TYPE_P(lifetime_or_options) == IS_ARRAY) { + zend_string *key; + zval *value; + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(lifetime_or_options), key, value) { + if (key) { + ZVAL_DEREF(value); + if(!strcasecmp("lifetime", ZSTR_VAL(key))) { + lifetime = zval_get_string(value); + found++; + } else if(!strcasecmp("path", ZSTR_VAL(key))) { + path = zval_get_string(value); + found++; + } else if(!strcasecmp("domain", ZSTR_VAL(key))) { + domain = zval_get_string(value); + found++; + } else if(!strcasecmp("secure", ZSTR_VAL(key))) { + secure = zval_is_true(value); + secure_null = 0; + found++; + } else if(!strcasecmp("httponly", ZSTR_VAL(key))) { + httponly = zval_is_true(value); + httponly_null = 0; + found++; + } else if(!strcasecmp("samesite", ZSTR_VAL(key))) { + samesite = zval_get_string(value); + found++; + } else { + php_error_docref(NULL, E_WARNING, "Unrecognized key '%s' found in the options array", ZSTR_VAL(key)); + } + } else { + php_error_docref(NULL, E_WARNING, "Numeric key found in the options array"); + } + } ZEND_HASH_FOREACH_END(); - ini_name = zend_string_init("session.cookie_lifetime", sizeof("session.cookie_lifetime") - 1, 0); - if (zend_alter_ini_entry(ini_name, Z_STR_P(lifetime), PHP_INI_USER, PHP_INI_STAGE_RUNTIME) == FAILURE) { - zend_string_release_ex(ini_name, 0); - RETURN_FALSE; + if (found == 0) { + php_error_docref(NULL, E_WARNING, "No valid keys were found in the options array"); + RETURN_FALSE; + } + } else { + lifetime = zval_get_string(lifetime_or_options); } - zend_string_release_ex(ini_name, 0); + if (lifetime) { + ini_name = zend_string_init("session.cookie_lifetime", sizeof("session.cookie_lifetime") - 1, 0); + result = zend_alter_ini_entry(ini_name, lifetime, PHP_INI_USER, PHP_INI_STAGE_RUNTIME); + zend_string_release(lifetime); + zend_string_release_ex(ini_name, 0); + if (result == FAILURE) { + RETURN_FALSE; + } + } if (path) { ini_name = zend_string_init("session.cookie_path", sizeof("session.cookie_path") - 1, 0); - if (zend_alter_ini_entry(ini_name, path, PHP_INI_USER, PHP_INI_STAGE_RUNTIME) == FAILURE) { - zend_string_release_ex(ini_name, 0); - RETURN_FALSE; + result = zend_alter_ini_entry(ini_name, path, PHP_INI_USER, PHP_INI_STAGE_RUNTIME); + if (found > 0) { + zend_string_release(path); } zend_string_release_ex(ini_name, 0); + if (result == FAILURE) { + RETURN_FALSE; + } } if (domain) { ini_name = zend_string_init("session.cookie_domain", sizeof("session.cookie_domain") - 1, 0); - if (zend_alter_ini_entry(ini_name, domain, PHP_INI_USER, PHP_INI_STAGE_RUNTIME) == FAILURE) { - zend_string_release_ex(ini_name, 0); - RETURN_FALSE; + result = zend_alter_ini_entry(ini_name, domain, PHP_INI_USER, PHP_INI_STAGE_RUNTIME); + if (found > 0) { + zend_string_release(domain); } zend_string_release_ex(ini_name, 0); + if (result == FAILURE) { + RETURN_FALSE; + } } - - if (argc > 3) { + if (!secure_null) { ini_name = zend_string_init("session.cookie_secure", sizeof("session.cookie_secure") - 1, 0); - if (zend_alter_ini_entry_chars(ini_name, secure ? "1" : "0", 1, PHP_INI_USER, PHP_INI_STAGE_RUNTIME) == FAILURE) { - zend_string_release_ex(ini_name, 0); + result = zend_alter_ini_entry_chars(ini_name, secure ? "1" : "0", 1, PHP_INI_USER, PHP_INI_STAGE_RUNTIME); + zend_string_release_ex(ini_name, 0); + if (result == FAILURE) { RETURN_FALSE; } - zend_string_release_ex(ini_name, 0); } - if (argc > 4) { + if (!httponly_null) { ini_name = zend_string_init("session.cookie_httponly", sizeof("session.cookie_httponly") - 1, 0); - if (zend_alter_ini_entry_chars(ini_name, httponly ? "1" : "0", 1, PHP_INI_USER, PHP_INI_STAGE_RUNTIME) == FAILURE) { - zend_string_release_ex(ini_name, 0); + result = zend_alter_ini_entry_chars(ini_name, httponly ? "1" : "0", 1, PHP_INI_USER, PHP_INI_STAGE_RUNTIME); + zend_string_release_ex(ini_name, 0); + if (result == FAILURE) { RETURN_FALSE; } + } + 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); + if (found > 0) { + zend_string_release(samesite); + } zend_string_release_ex(ini_name, 0); + if (result == FAILURE) { + RETURN_FALSE; + } } - if (argc > 5) { - ini_name = zend_string_init("session.cookie_samesite", sizeof("session.cookie_samesite") - 1, 0); - zend_alter_ini_entry(ini_name, samesite, PHP_INI_USER, PHP_INI_STAGE_RUNTIME); - zend_string_release(ini_name); - } - RETURN_TRUE; } /* }}} */ @@ -2640,12 +2703,11 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_session_cache_expire, 0, 0, 0) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_session_set_cookie_params, 0, 0, 1) - ZEND_ARG_INFO(0, lifetime) + ZEND_ARG_INFO(0, lifetime_or_options) ZEND_ARG_INFO(0, path) ZEND_ARG_INFO(0, domain) ZEND_ARG_INFO(0, secure) ZEND_ARG_INFO(0, httponly) - ZEND_ARG_INFO(0, samesite) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO(arginfo_session_class_open, 0) diff --git a/ext/session/tests/session_get_cookie_params_basic.phpt b/ext/session/tests/session_get_cookie_params_basic.phpt index 9dcde4ec350b2..6188e06e6df8d 100644 --- a/ext/session/tests/session_get_cookie_params_basic.phpt +++ b/ext/session/tests/session_get_cookie_params_basic.phpt @@ -23,9 +23,17 @@ ob_start(); echo "*** Testing session_get_cookie_params() : basic functionality ***\n"; var_dump(session_get_cookie_params()); -var_dump(session_set_cookie_params(3600, "/path", "blah", FALSE, FALSE, "foo")); +var_dump(session_set_cookie_params(3600, "/path", "blah", FALSE, FALSE)); var_dump(session_get_cookie_params()); -var_dump(session_set_cookie_params(1234567890, "/guff", "foo", TRUE, TRUE, "blah")); +var_dump(session_set_cookie_params(1234567890, "/guff", "foo", TRUE, TRUE)); +var_dump(session_get_cookie_params()); +var_dump(session_set_cookie_params([ + "lifetime" => 123, + "path" => "/bar", + "domain" => "baz", + "secure" => FALSE, + "httponly" => FALSE, + "samesite" => "please"])); var_dump(session_get_cookie_params()); echo "Done"; @@ -60,7 +68,7 @@ array(6) { ["httponly"]=> bool(false) ["samesite"]=> - string(3) "foo" + string(0) "" } bool(true) array(6) { @@ -75,6 +83,21 @@ array(6) { ["httponly"]=> bool(true) ["samesite"]=> - string(4) "blah" + string(0) "" +} +bool(true) +array(6) { + ["lifetime"]=> + int(123) + ["path"]=> + string(4) "/bar" + ["domain"]=> + string(3) "baz" + ["secure"]=> + bool(false) + ["httponly"]=> + bool(false) + ["samesite"]=> + string(6) "please" } Done diff --git a/ext/session/tests/session_set_cookie_params_variation3.phpt b/ext/session/tests/session_set_cookie_params_variation3.phpt index e4e26b91b2ace..17d1e6a7713d0 100644 --- a/ext/session/tests/session_set_cookie_params_variation3.phpt +++ b/ext/session/tests/session_set_cookie_params_variation3.phpt @@ -10,7 +10,7 @@ session.cookie_domain=foo ob_start(); /* - * Prototype : void session_set_cookie_params(int $lifetime [, string $path [, string $domain [, bool $secure [, bool $httponly[, string $samesite]]]]]) + * Prototype : void session_set_cookie_params(int $lifetime [, string $path [, string $domain [, bool $secure [, bool $httponly]]]]) * Description : Set the session cookie parameters * Source code : ext/session/session.c */ diff --git a/ext/session/tests/session_set_cookie_params_variation5.phpt b/ext/session/tests/session_set_cookie_params_variation5.phpt index f3374d0daaad1..ffdd29db2d48c 100644 --- a/ext/session/tests/session_set_cookie_params_variation5.phpt +++ b/ext/session/tests/session_set_cookie_params_variation5.phpt @@ -10,7 +10,7 @@ session.cookie_httponly=TRUE ob_start(); /* - * Prototype : void session_set_cookie_params(int $lifetime [, string $path [, string $domain [, bool $secure [, bool $httponly[, string $samesite]]]]]) + * Prototype : void session_set_cookie_params(int $lifetime [, string $path [, string $domain [, bool $secure [, bool $httponly]]]]) * Description : Set the session cookie parameters * Source code : ext/session/session.c */ diff --git a/ext/session/tests/session_set_cookie_params_variation6.phpt b/ext/session/tests/session_set_cookie_params_variation6.phpt index 7fe1e009eea6d..b94380d37062c 100644 --- a/ext/session/tests/session_set_cookie_params_variation6.phpt +++ b/ext/session/tests/session_set_cookie_params_variation6.phpt @@ -10,7 +10,7 @@ session.cookie_samesite=test ob_start(); /* - * Prototype : void session_set_cookie_params(int $lifetime [, string $path [, string $domain [, bool $secure [, bool $samesite[, string $samesite]]]]]) + * Prototype : void session_set_cookie_params(array $options) * Description : Set the session cookie parameters * Source code : ext/session/session.c */ @@ -18,15 +18,15 @@ ob_start(); echo "*** Testing session_set_cookie_params() : variation ***\n"; var_dump(ini_get("session.cookie_samesite")); -var_dump(session_set_cookie_params(3600, "/path", "blah", FALSE, FALSE, "nothing")); +var_dump(session_set_cookie_params(["samesite" => "nothing"])); var_dump(ini_get("session.cookie_samesite")); var_dump(session_start()); var_dump(ini_get("session.cookie_samesite")); -var_dump(session_set_cookie_params(3600, "/path", "blah", FALSE, TRUE, "test")); +var_dump(session_set_cookie_params(["samesite" => "test"])); var_dump(ini_get("session.cookie_samesite")); var_dump(session_destroy()); var_dump(ini_get("session.cookie_samesite")); -var_dump(session_set_cookie_params(3600, "/path", "blah", FALSE, FALSE, "other")); +var_dump(session_set_cookie_params(["samesite" => "other"])); var_dump(ini_get("session.cookie_samesite")); echo "Done"; diff --git a/ext/session/tests/session_set_cookie_params_variation7.phpt b/ext/session/tests/session_set_cookie_params_variation7.phpt new file mode 100644 index 0000000000000..ebd9b71df6074 --- /dev/null +++ b/ext/session/tests/session_set_cookie_params_variation7.phpt @@ -0,0 +1,60 @@ +--TEST-- +Test session_set_cookie_params() function : array parameter variation +--INI-- +session.cookie_lifetime=0 +session.cookie_path="/" +session.cookie_domain="" +session.cookie_secure=0 +session.cookie_httponly=0 +session.cookie_samesite="" +--SKIPIF-- + +--FILE-- + true])); + +var_dump(ini_get("session.cookie_secure")); +var_dump(ini_get("session.cookie_samesite")); +var_dump(session_set_cookie_params(["secure" => true, "samesite" => "please"])); +var_dump(ini_get("session.cookie_secure")); +var_dump(ini_get("session.cookie_samesite")); + +var_dump(ini_get("session.cookie_lifetime")); +var_dump(session_set_cookie_params(["lifetime" => 42])); +var_dump(ini_get("session.cookie_lifetime")); + +echo "Done"; +ob_end_flush(); +?> +--EXPECTF-- +*** Testing session_set_cookie_params() : array parameter variation *** + +Warning: session_set_cookie_params(): No valid keys were found in the options array in %s +bool(false) + +Warning: session_set_cookie_params(): Unrecognized key 'unknown_key' found in the options array in %s + +Warning: session_set_cookie_params(): No valid keys were found in the options array in %s +bool(false) +string(1) "0" +string(0) "" +bool(true) +string(1) "1" +string(6) "please" +string(1) "0" +bool(true) +string(2) "42" +Done diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 6849014122996..eed29ee8eef31 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -1433,23 +1433,21 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_setcookie, 0, 0, 1) ZEND_ARG_INFO(0, name) ZEND_ARG_INFO(0, value) - ZEND_ARG_INFO(0, expires) + ZEND_ARG_INFO(0, expires_or_options) ZEND_ARG_INFO(0, path) ZEND_ARG_INFO(0, domain) ZEND_ARG_INFO(0, secure) ZEND_ARG_INFO(0, httponly) - ZEND_ARG_INFO(0, samesite) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_setrawcookie, 0, 0, 1) ZEND_ARG_INFO(0, name) ZEND_ARG_INFO(0, value) - ZEND_ARG_INFO(0, expires) + ZEND_ARG_INFO(0, expires_or_options) ZEND_ARG_INFO(0, path) ZEND_ARG_INFO(0, domain) ZEND_ARG_INFO(0, secure) ZEND_ARG_INFO(0, httponly) - ZEND_ARG_INFO(0, samesite) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(arginfo_headers_sent, 0, 0, 0) diff --git a/ext/standard/head.c b/ext/standard/head.c index 487cc648eb9c1..551dab63726ab 100644 --- a/ext/standard/head.c +++ b/ext/standard/head.c @@ -80,7 +80,7 @@ PHPAPI int php_header(void) } } -PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, zend_string *path, zend_string *domain, int secure, int url_encode, int httponly, zend_string *samesite) +PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, zend_string *path, zend_string *domain, int secure, int httponly, zend_string *samesite, int url_encode) { char *cookie; size_t len = sizeof("Set-Cookie: "); @@ -207,61 +207,154 @@ PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, return result; } +static int php_head_parse_cookie_options_array(zval *options, zend_long *expires, zend_string **path, zend_string **domain, zend_bool *secure, zend_bool *httponly, zend_string **samesite) { + int found = 0; + zend_string *key; + zval *value; + + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(options), key, value) { + if (key) { + ZVAL_DEREF(value); + if(!strcasecmp("expires", ZSTR_VAL(key))) { + *expires = zval_get_long(value); + found++; + } else if(!strcasecmp("path", ZSTR_VAL(key))) { + *path = zval_get_string(value); + found++; + } else if(!strcasecmp("domain", ZSTR_VAL(key))) { + *domain = zval_get_string(value); + found++; + } else if(!strcasecmp("secure", ZSTR_VAL(key))) { + *secure = zval_is_true(value); + found++; + } else if(!strcasecmp("httponly", ZSTR_VAL(key))) { + *httponly = zval_is_true(value); + found++; + } else if(!strcasecmp("samesite", ZSTR_VAL(key))) { + *samesite = zval_get_string(value); + found++; + } else { + php_error_docref(NULL, E_WARNING, "Unrecognized key '%s' found in the options array", ZSTR_VAL(key)); + } + } else { + php_error_docref(NULL, E_WARNING, "Numeric key found in the options array"); + } + } ZEND_HASH_FOREACH_END(); + + /* Array is not empty but no valid keys were found */ + if (found == 0 && zend_hash_num_elements(Z_ARRVAL_P(options)) > 0) { + php_error_docref(NULL, E_WARNING, "No valid options were found in the given array"); + return 0; + } + + return 1; +} -/* php_set_cookie(name, value, expires, path, domain, secure) */ -/* {{{ proto bool setcookie(string name [, string value [, int expires [, string path [, string domain [, bool secure[, bool httponly[, string samesite]]]]]]]) +/* {{{ proto bool setcookie(string name [, string value [, int expires [, string path [, string domain [, bool secure[, bool httponly]]]]]]) + setcookie(string name [, string value [, array options]]) Send a cookie */ PHP_FUNCTION(setcookie) { + zval *expires_or_options = NULL; zend_string *name, *value = NULL, *path = NULL, *domain = NULL, *samesite = NULL; zend_long expires = 0; - zend_bool secure = 0, httponly = 0; + zend_bool secure = 0, httponly = 0, options_array = 0; - ZEND_PARSE_PARAMETERS_START(1, 8) + ZEND_PARSE_PARAMETERS_START(1, 7) Z_PARAM_STR(name) Z_PARAM_OPTIONAL Z_PARAM_STR(value) - Z_PARAM_LONG(expires) + Z_PARAM_ZVAL(expires_or_options) Z_PARAM_STR(path) Z_PARAM_STR(domain) Z_PARAM_BOOL(secure) Z_PARAM_BOOL(httponly) - Z_PARAM_STR(samesite) ZEND_PARSE_PARAMETERS_END(); - if (php_setcookie(name, value, expires, path, domain, secure, 1, httponly, samesite) == SUCCESS) { + if (expires_or_options) { + if (Z_TYPE_P(expires_or_options) == IS_ARRAY) { + options_array = 1; + if (!php_head_parse_cookie_options_array(expires_or_options, &expires, &path, &domain, &secure, &httponly, &samesite)) { + RETVAL_FALSE; + goto cleanup; + } + } else { + expires = Z_LVAL_P(expires_or_options); + } + } + + if (php_setcookie(name, value, expires, path, domain, secure, httponly, samesite, 1) == SUCCESS) { RETVAL_TRUE; } else { RETVAL_FALSE; } + +cleanup: + if (options_array) { + if (path) { + zend_string_release(path); + } + if (domain) { + zend_string_release(domain); + } + if (samesite) { + zend_string_release(samesite); + } + } } /* }}} */ -/* {{{ proto bool setrawcookie(string name [, string value [, int expires [, string path [, string domain [, bool secure[, bool httponly[, string samesite]]]]]]]) +/* {{{ proto bool setrawcookie(string name [, string value [, int expires [, string path [, string domain [, bool secure[, bool httponly]]]]]]) + setrawcookie(string name [, string value [, array options]]) Send a cookie with no url encoding of the value */ PHP_FUNCTION(setrawcookie) { + zval *expires_or_options = NULL; zend_string *name, *value = NULL, *path = NULL, *domain = NULL, *samesite = NULL; zend_long expires = 0; - zend_bool secure = 0, httponly = 0; + zend_bool secure = 0, httponly = 0, options_array = 0; - ZEND_PARSE_PARAMETERS_START(1, 8) + ZEND_PARSE_PARAMETERS_START(1, 7) Z_PARAM_STR(name) Z_PARAM_OPTIONAL Z_PARAM_STR(value) - Z_PARAM_LONG(expires) + Z_PARAM_ZVAL(expires_or_options) Z_PARAM_STR(path) Z_PARAM_STR(domain) Z_PARAM_BOOL(secure) Z_PARAM_BOOL(httponly) - Z_PARAM_STR(samesite) ZEND_PARSE_PARAMETERS_END(); - if (php_setcookie(name, value, expires, path, domain, secure, 0, httponly, samesite) == SUCCESS) { + if (expires_or_options) { + if (Z_TYPE_P(expires_or_options) == IS_ARRAY) { + options_array = 1; + if (!php_head_parse_cookie_options_array(expires_or_options, &expires, &path, &domain, &secure, &httponly, &samesite)) { + RETVAL_FALSE; + goto cleanup; + } + } else { + expires = Z_LVAL_P(expires_or_options); + } + } + + if (php_setcookie(name, value, expires, path, domain, secure, httponly, samesite, 0) == SUCCESS) { RETVAL_TRUE; } else { RETVAL_FALSE; } + +cleanup: + if (options_array) { + if (path) { + zend_string_release(path); + } + if (domain) { + zend_string_release(domain); + } + if (samesite) { + zend_string_release(samesite); + } + } } /* }}} */ diff --git a/ext/standard/head.h b/ext/standard/head.h index 17d15bf2ca4f0..4cf7e8d6d0073 100644 --- a/ext/standard/head.h +++ b/ext/standard/head.h @@ -39,6 +39,6 @@ PHP_FUNCTION(headers_list); PHP_FUNCTION(http_response_code); PHPAPI int php_header(void); -PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, zend_string *path, zend_string *domain, int secure, int url_encode, int httponly, zend_string *samesite); +PHPAPI int php_setcookie(zend_string *name, zend_string *value, time_t expires, zend_string *path, zend_string *domain, int secure, int httponly, zend_string *samesite, int url_encode); #endif diff --git a/ext/standard/tests/network/setcookie.phpt b/ext/standard/tests/network/setcookie.phpt index 167e65f0a7357..d41bed01f4e9e 100644 --- a/ext/standard/tests/network/setcookie.phpt +++ b/ext/standard/tests/network/setcookie.phpt @@ -17,6 +17,8 @@ setcookie('name', 'value', 0, '', 'domain.tld'); setcookie('name', 'value', 0, '', '', TRUE); 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']); $expected = array( 'Set-Cookie: name=deleted; expires='.date('D, d-M-Y H:i:s', 1).' GMT; Max-Age=0', @@ -30,7 +32,9 @@ $expected = array( 'Set-Cookie: name=value; path=/path/', 'Set-Cookie: name=value; domain=domain.tld', 'Set-Cookie: name=value; secure', - 'Set-Cookie: name=value; HttpOnly' + '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' ); $headers = headers_list(); diff --git a/ext/standard/tests/network/setcookie_error.phpt b/ext/standard/tests/network/setcookie_error.phpt new file mode 100644 index 0000000000000..1cbdf9ef0caee --- /dev/null +++ b/ext/standard/tests/network/setcookie_error.phpt @@ -0,0 +1,26 @@ +--TEST-- +setcookie() array variant error tests +--INI-- +date.timezone=UTC +--FILE-- + 'only']); +// Numeric key and no valid keys +setcookie('name', 'value', [0 => 'numeric_key']); +// Unrecognized key +setcookie('name', 'value', ['path' => '/path/', 'foo' => 'bar']); + +--EXPECTF-- +Warning: setcookie(): Unrecognized key 'unknown_key' found in the options array in %s + +Warning: setcookie(): No valid options were found in the given array in %s + +Warning: setcookie(): Numeric key found in the options array in %s + +Warning: setcookie(): No valid options were found in the given array in %s + +Warning: setcookie(): Unrecognized key 'foo' found in the options array in %s