Commit b9a0228bd84 for php
commit b9a0228bd848885eba18a10e569d4d13746fc7e9
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Wed Oct 14 06:28:08 2026 -0400
ext/session: reject cookie separators in cookie_path and cookie_domain (#22925)
php_session_send_cookie() appends session.cookie_path and
session.cookie_domain to the Set-Cookie header verbatim, and neither
setting rejected the separators that terminate a cookie attribute. An
application could therefore append its own Domain, Path, HttpOnly,
Secure, or SameSite attribute through either value. cookie_path now
rejects ";" and CR/LF, which RFC 6265 path-value excludes, while keeping
the comma it allows; cookie_domain takes the set setcookie() enforces on
$domain.
Closes GH-22925
diff --git a/UPGRADING b/UPGRADING
index 46cf4cafab1..da184a86265 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -27,6 +27,13 @@ PHP 8.7 UPGRADE NOTES
not present in the result set. It previously reported the condition
through PDO::ATTR_ERRMODE and returned false.
+- Session:
+ . Setting session.cookie_path to a value containing one of ";\r\n", or
+ session.cookie_domain to a value containing one of ",; \t\r\n\013\014",
+ now emits a warning and leaves the setting unchanged. Previously such a
+ value was appended to the Set-Cookie header verbatim, which let an
+ application terminate one attribute and append further ones.
+
- Standard:
. The number of filters that can be chained in a php://filter URL is limited
to 16 by default. Set the stream context option max_filter_count to change
diff --git a/ext/session/session.c b/ext/session/session.c
index 5d62245755e..513d8229646 100644
--- a/ext/session/session.c
+++ b/ext/session/session.c
@@ -97,6 +97,12 @@ zend_class_entry *php_session_update_timestamp_iface_entry;
#define SESSION_FORBIDDEN_CHARS "=,;.[ \t\r\n\013\014"
#define SESSION_FORBIDDEN_CHARS_FOR_ERROR_MSG "=,;.[ \\t\\r\\n\\013\\014"
+#define SESSION_FORBIDDEN_COOKIE_PATH_CHARS ";\r\n"
+#define SESSION_FORBIDDEN_COOKIE_PATH_CHARS_FOR_ERROR_MSG ";\\r\\n"
+
+#define SESSION_FORBIDDEN_COOKIE_DOMAIN_CHARS ",; \t\r\n\013\014"
+#define SESSION_FORBIDDEN_COOKIE_DOMAIN_CHARS_FOR_ERROR_MSG ",; \\t\\r\\n\\013\\014"
+
#define APPLY_TRANS_SID (PS(use_trans_sid) && !PS(use_only_cookies))
static zend_result php_session_send_cookie(void);
@@ -769,6 +775,36 @@ static PHP_INI_MH(OnUpdateSessionStr)
return OnUpdateStr(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage);
}
+static bool php_session_cookie_attr_is_valid(const zend_ini_entry *entry, const zend_string *new_value, int stage, const char *forbidden, const char *forbidden_msg)
+{
+ if (new_value && strpbrk(ZSTR_VAL(new_value), forbidden) != NULL) {
+ if (stage != ZEND_INI_STAGE_DEACTIVATE) {
+ php_error_docref(NULL, E_WARNING, "\"%s\" must not contain any of the following characters \"%s\"", ZSTR_VAL(entry->name), forbidden_msg);
+ }
+ return false;
+ }
+
+ return true;
+}
+
+static PHP_INI_MH(OnUpdateSessionCookiePath)
+{
+ if (!php_session_cookie_attr_is_valid(entry, new_value, stage, SESSION_FORBIDDEN_COOKIE_PATH_CHARS, SESSION_FORBIDDEN_COOKIE_PATH_CHARS_FOR_ERROR_MSG)) {
+ return FAILURE;
+ }
+
+ return OnUpdateSessionStr(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage);
+}
+
+static PHP_INI_MH(OnUpdateSessionCookieDomain)
+{
+ if (!php_session_cookie_attr_is_valid(entry, new_value, stage, SESSION_FORBIDDEN_COOKIE_DOMAIN_CHARS, SESSION_FORBIDDEN_COOKIE_DOMAIN_CHARS_FOR_ERROR_MSG)) {
+ return FAILURE;
+ }
+
+ return OnUpdateSessionStr(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage);
+}
+
static PHP_INI_MH(OnUpdateSessionSameSite)
{
SESSION_CHECK_ACTIVE_STATE;
@@ -956,8 +992,8 @@ PHP_INI_BEGIN()
STD_PHP_INI_ENTRY("session.gc_maxlifetime", "1440", PHP_INI_ALL, OnUpdateSessionLong, gc_maxlifetime, php_ps_globals, ps_globals)
PHP_INI_ENTRY("session.serialize_handler", "php", PHP_INI_ALL, OnUpdateSerializer)
STD_PHP_INI_ENTRY("session.cookie_lifetime", "0", PHP_INI_ALL, OnUpdateCookieLifetime, cookie_lifetime, php_ps_globals, ps_globals)
- STD_PHP_INI_ENTRY("session.cookie_path", "/", PHP_INI_ALL, OnUpdateSessionStr, cookie_path, php_ps_globals, ps_globals)
- STD_PHP_INI_ENTRY("session.cookie_domain", "", PHP_INI_ALL, OnUpdateSessionStr, cookie_domain, php_ps_globals, ps_globals)
+ STD_PHP_INI_ENTRY("session.cookie_path", "/", PHP_INI_ALL, OnUpdateSessionCookiePath, cookie_path, php_ps_globals, ps_globals)
+ STD_PHP_INI_ENTRY("session.cookie_domain", "", PHP_INI_ALL, OnUpdateSessionCookieDomain, cookie_domain, php_ps_globals, ps_globals)
STD_PHP_INI_BOOLEAN("session.cookie_secure", "0", PHP_INI_ALL, OnUpdateSessionBool, cookie_secure, php_ps_globals, ps_globals)
STD_PHP_INI_BOOLEAN("session.cookie_partitioned", "0", PHP_INI_ALL, OnUpdateSessionBool, cookie_partitioned, php_ps_globals, ps_globals)
STD_PHP_INI_BOOLEAN("session.cookie_httponly", "1", PHP_INI_ALL, OnUpdateSessionBool, cookie_httponly, php_ps_globals, ps_globals)
diff --git a/ext/session/tests/session_cookie_params_forbidden_chars.phpt b/ext/session/tests/session_cookie_params_forbidden_chars.phpt
new file mode 100644
index 00000000000..9b757df4b44
--- /dev/null
+++ b/ext/session/tests/session_cookie_params_forbidden_chars.phpt
@@ -0,0 +1,43 @@
+--TEST--
+session.cookie_path and session.cookie_domain reject cookie separators
+--INI--
+session.save_handler=files
+session.name=PHPSESSID
+session.gc_probability=0
+--EXTENSIONS--
+session
+--FILE--
+<?php
+
+ob_start();
+
+var_dump(session_set_cookie_params(['path' => '/; Domain=evil.example']));
+var_dump(session_set_cookie_params(['domain' => 'example.com; HttpOnly']));
+var_dump(session_set_cookie_params(['path' => "/\r\nX-Injected: yes"]));
+var_dump(ini_set('session.cookie_domain', "example.com\tevil"));
+
+$params = session_get_cookie_params();
+var_dump($params['path'], $params['domain']);
+
+var_dump(session_set_cookie_params(['path' => '/foo,bar', 'domain' => 'example.com']));
+$params = session_get_cookie_params();
+var_dump($params['path'], $params['domain']);
+
+?>
+--EXPECTF--
+Warning: session_set_cookie_params(): "session.cookie_path" must not contain any of the following characters ";\r\n" in %s on line %d
+bool(false)
+
+Warning: session_set_cookie_params(): "session.cookie_domain" must not contain any of the following characters ",; \t\r\n\013\014" in %s on line %d
+bool(false)
+
+Warning: session_set_cookie_params(): "session.cookie_path" must not contain any of the following characters ";\r\n" in %s on line %d
+bool(false)
+
+Warning: ini_set(): "session.cookie_domain" must not contain any of the following characters ",; \t\r\n\013\014" in %s on line %d
+bool(false)
+string(1) "/"
+string(0) ""
+bool(true)
+string(8) "/foo,bar"
+string(11) "example.com"