Commit 27ead919e07 for php.net

commit 27ead919e07dd5b6e975b49262b56f2b102f16f3
Author: Jorg Adam Sowa <jorg.sowa@gmail.com>
Date:   Tue May 19 14:20:05 2026 +0200

    ext/session: secure session configuration defaults (RFC) (#21938)

    * ext/session: secure session configuration defaults (PHP 8.6 RFC)

    Implements the "Secure Session Configuration Defaults" RFC by changing
    three INI defaults to provide secure session behavior out of the box:

    - session.use_strict_mode: 0 -> 1 (mitigates session fixation)
    - session.cookie_httponly: 0 -> 1 (mitigates XSS access to session cookie)
    - session.cookie_samesite: "" -> "Lax" (mitigates CSRF)

    RFC: https://wiki.php.net/rfc/session_security_defaults

diff --git a/NEWS b/NEWS
index 1200d086067..e37effff3af 100644
--- a/NEWS
+++ b/NEWS
@@ -156,6 +156,8 @@ PHP                                                                        NEWS
     (Girgias)
   . Null bytes in session.cookie_path, session.cookie_domain, and
     session.cache_limiter are now rejected with a warning. (jorgsowa)
+  . Changed defaults of session.use_strict_mode (now 1), session.cookie_httponly
+    (now 1) and session.cookie_samesite (now "Lax"). (jorgsowa)

 - Soap:
   . Soap::__setCookie() when cookie name is a digit is now not stored and
diff --git a/UPGRADING b/UPGRADING
index 1bba53274c1..a3b22d8c056 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -94,6 +94,24 @@ PHP 8.6 UPGRADE NOTES
     comparison. Custom session handlers that rely on write() being called
     with empty data (e.g. to destroy the session) should implement the same
     logic in their updateTimestamp() method.
+  . The defaults of three session INI settings have changed to provide secure
+    behavior out of the box:
+      - session.use_strict_mode is now 1 (was 0). Strict mode rejects
+        uninitialized session IDs, mitigating session fixation. Custom session
+        handlers that previously relied on accepting externally supplied IDs
+        without a corresponding storage entry must either implement
+        validateId() / create_sid() or explicitly set this to 0.
+      - session.cookie_httponly is now 1 (was 0). Session cookies are no
+        longer accessible to JavaScript via document.cookie. Applications
+        that read the session cookie from JavaScript must explicitly set
+        this to 0.
+      - session.cookie_samesite is now "Lax" (was unset). Session cookies
+        are no longer sent on cross-site requests other than top-level
+        navigations using safe HTTP methods. Applications that depend on
+        session cookies being sent on cross-site POST submissions must
+        explicitly set this to "None" (and also set session.cookie_secure
+        to 1).
+    RFC: https://wiki.php.net/rfc/session_security_defaults

 - SPL:
   . SplObjectStorage::getHash() implementations may no longer mutate any
diff --git a/ext/session/session.c b/ext/session/session.c
index aa6cfb311af..1fdfc5d1073 100644
--- a/ext/session/session.c
+++ b/ext/session/session.c
@@ -928,11 +928,11 @@ PHP_INI_BEGIN()
 	STD_PHP_INI_ENTRY("session.cookie_domain",        "",          PHP_INI_ALL,    OnUpdateSessionStr,           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",    "0",         PHP_INI_ALL,    OnUpdateSessionBool,          cookie_httponly,    php_ps_globals, ps_globals)
-	STD_PHP_INI_ENTRY("session.cookie_samesite",      "",          PHP_INI_ALL,    OnUpdateSessionSameSite,      cookie_samesite,    php_ps_globals, ps_globals)
+	STD_PHP_INI_BOOLEAN("session.cookie_httponly",    "1",         PHP_INI_ALL,    OnUpdateSessionBool,          cookie_httponly,    php_ps_globals, ps_globals)
+	STD_PHP_INI_ENTRY("session.cookie_samesite",      "Lax",       PHP_INI_ALL,    OnUpdateSessionSameSite,      cookie_samesite,    php_ps_globals, ps_globals)
 	STD_PHP_INI_BOOLEAN("session.use_cookies",        "1",         PHP_INI_ALL,    OnUpdateSessionBool,          use_cookies,        php_ps_globals, ps_globals)
 	STD_PHP_INI_BOOLEAN("session.use_only_cookies",   "1",         PHP_INI_ALL,    OnUpdateUseOnlyCookies,       use_only_cookies,   php_ps_globals, ps_globals)
-	STD_PHP_INI_BOOLEAN("session.use_strict_mode",    "0",         PHP_INI_ALL,    OnUpdateSessionBool,          use_strict_mode,    php_ps_globals, ps_globals)
+	STD_PHP_INI_BOOLEAN("session.use_strict_mode",    "1",         PHP_INI_ALL,    OnUpdateSessionBool,          use_strict_mode,    php_ps_globals, ps_globals)
 	STD_PHP_INI_ENTRY("session.referer_check",        "",          PHP_INI_ALL,    OnUpdateRefererCheck,         extern_referer_chk, php_ps_globals, ps_globals)
 	STD_PHP_INI_ENTRY("session.cache_limiter",        "nocache",   PHP_INI_ALL,    OnUpdateSessionStr,           cache_limiter,      php_ps_globals, ps_globals)
 	STD_PHP_INI_ENTRY("session.cache_expire",         "180",       PHP_INI_ALL,    OnUpdateSessionLong,          cache_expire,       php_ps_globals, ps_globals)
diff --git a/ext/session/tests/bug74892.phpt b/ext/session/tests/bug74892.phpt
index 916120cb4d4..7a2211210b4 100644
--- a/ext/session/tests/bug74892.phpt
+++ b/ext/session/tests/bug74892.phpt
@@ -4,6 +4,7 @@
 session.use_cookies=0
 session.use_only_cookies=0
 session.use_trans_sid=1
+session.use_strict_mode=0
 --EXTENSIONS--
 session
 --SKIPIF--
diff --git a/ext/session/tests/bug80774.phpt b/ext/session/tests/bug80774.phpt
index 7dd86add02b..ba5d39e1953 100644
--- a/ext/session/tests/bug80774.phpt
+++ b/ext/session/tests/bug80774.phpt
@@ -2,6 +2,8 @@
 Bug #80774 (session_name() problem with backslash)
 --EXTENSIONS--
 session
+--INI--
+session.use_strict_mode=0
 --FILE--
 <?php
 session_name("foo\\bar");
@@ -9,5 +11,5 @@
 session_start();
 ?>
 --EXPECTHEADERS--
-Set-Cookie: foo\bar=12345; path=/
+Set-Cookie: foo\bar=12345; path=/; HttpOnly; SameSite=Lax
 --EXPECT--
diff --git a/ext/session/tests/gh9200.phpt b/ext/session/tests/gh9200.phpt
index fd6e28ab8b9..7e458176d10 100644
--- a/ext/session/tests/gh9200.phpt
+++ b/ext/session/tests/gh9200.phpt
@@ -2,6 +2,7 @@
 GH-9200: setcookie has an obsolete expires date format
 --INI--
 session.cookie_lifetime=3600
+session.use_strict_mode=0
 --EXTENSIONS--
 session
 --CGI--
@@ -12,7 +13,7 @@
 session_start();

 foreach (headers_list() as $header) {
-	if (preg_match('/^Set-Cookie: foo=bar; expires=(Mon|Tue|Wed|Thu|Fri|Sat|Sun), [0-9][0-9] (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) 2[0-9][0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9] GMT; Max-Age=3600; path=\\/$/', $header)) {
+	if (preg_match('/^Set-Cookie: foo=bar; expires=(Mon|Tue|Wed|Thu|Fri|Sat|Sun), [0-9][0-9] (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) 2[0-9][0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9] GMT; Max-Age=3600; path=\/; HttpOnly; SameSite=Lax$/', $header)) {
 		echo "Success", PHP_EOL;
 		exit;
 	}
diff --git a/ext/session/tests/mod_files/gc_dirdepth2.phpt b/ext/session/tests/mod_files/gc_dirdepth2.phpt
index a8724285125..d858c1f95db 100644
--- a/ext/session/tests/mod_files/gc_dirdepth2.phpt
+++ b/ext/session/tests/mod_files/gc_dirdepth2.phpt
@@ -7,6 +7,7 @@
 --INI--
 session.gc_probability=0
 session.gc_maxlifetime=10
+session.use_strict_mode=0
 --FILE--
 <?php
 $base = __DIR__ . '/gc_dirdepth2_test';
diff --git a/ext/session/tests/mod_files/gc_dirdepth_disabled.phpt b/ext/session/tests/mod_files/gc_dirdepth_disabled.phpt
index 81c62430d15..dc3d4720dd2 100644
--- a/ext/session/tests/mod_files/gc_dirdepth_disabled.phpt
+++ b/ext/session/tests/mod_files/gc_dirdepth_disabled.phpt
@@ -7,6 +7,7 @@
 --INI--
 session.gc_probability=0
 session.gc_maxlifetime=1
+session.use_strict_mode=0
 --FILE--
 <?php

diff --git a/ext/session/tests/mod_files/gc_dirdepth_multi_subdir_count.phpt b/ext/session/tests/mod_files/gc_dirdepth_multi_subdir_count.phpt
index 1ba047502f5..c5e916c86fd 100644
--- a/ext/session/tests/mod_files/gc_dirdepth_multi_subdir_count.phpt
+++ b/ext/session/tests/mod_files/gc_dirdepth_multi_subdir_count.phpt
@@ -7,6 +7,7 @@
 --INI--
 session.gc_probability=0
 session.gc_maxlifetime=10
+session.use_strict_mode=0
 --FILE--
 <?php
 $base = __DIR__ . '/gc_multi_subdir_test';
diff --git a/ext/session/tests/mod_files/gc_dirdepth_selective.phpt b/ext/session/tests/mod_files/gc_dirdepth_selective.phpt
index a173324171e..1f0785d66fe 100644
--- a/ext/session/tests/mod_files/gc_dirdepth_selective.phpt
+++ b/ext/session/tests/mod_files/gc_dirdepth_selective.phpt
@@ -7,6 +7,7 @@
 --INI--
 session.gc_probability=0
 session.gc_maxlifetime=10
+session.use_strict_mode=0
 --FILE--
 <?php
 $base = __DIR__ . '/gc_selective_test';
diff --git a/ext/session/tests/session_regenerate_id_cookie.phpt b/ext/session/tests/session_regenerate_id_cookie.phpt
index 6516ad7061d..32706047539 100644
--- a/ext/session/tests/session_regenerate_id_cookie.phpt
+++ b/ext/session/tests/session_regenerate_id_cookie.phpt
@@ -67,14 +67,14 @@ function find_cookie_header() {
 Expires: %s
 Cache-Control: no-store, no-cache, must-revalidate
 Pragma: no-cache
-Set-Cookie: PHPSESSID=%s; path=/
+Set-Cookie: PHPSESSID=%s; path=/; HttpOnly; SameSite=Lax
 Content-type: text/html; charset=UTF-8

 bool(true)
-Set-Cookie: PHPSESSID=%s; path=/
+Set-Cookie: PHPSESSID=%s; path=/; HttpOnly; SameSite=Lax
 bool(true)
 bool(true)
-Set-Cookie: PHPSESSID=%s; path=/
+Set-Cookie: PHPSESSID=%s; path=/; HttpOnly; SameSite=Lax
 bool(true)
 bool(true)
 string(32) "%s"
diff --git a/ext/session/tests/session_set_cookie_params_invalid_ini.phpt b/ext/session/tests/session_set_cookie_params_invalid_ini.phpt
index 61728c342ea..8ebef4a5209 100644
--- a/ext/session/tests/session_set_cookie_params_invalid_ini.phpt
+++ b/ext/session/tests/session_set_cookie_params_invalid_ini.phpt
@@ -18,5 +18,5 @@
 ?>
 --EXPECTF--
 Warning: PHP Startup: session.cookie_samesite must be "Strict", "Lax", "None", or "" in Unknown on line 0
-string(0) ""
+string(3) "Lax"
 Done
diff --git a/ext/session/tests/session_start_partitioned_headers.phpt b/ext/session/tests/session_start_partitioned_headers.phpt
index 6fa3815aa85..4a2c7dcf878 100644
--- a/ext/session/tests/session_start_partitioned_headers.phpt
+++ b/ext/session/tests/session_start_partitioned_headers.phpt
@@ -2,6 +2,8 @@
 session_start() with partitioned cookies - header test
 --EXTENSIONS--
 session
+--INI--
+session.use_strict_mode=0
 --FILE--
 <?php
 session_id('12345');
@@ -9,5 +11,5 @@
 session_start();
 ?>
 --EXPECTHEADERS--
-Set-Cookie: PHPSESSID=12345; path=/; secure; Partitioned
+Set-Cookie: PHPSESSID=12345; path=/; secure; Partitioned; HttpOnly; SameSite=Lax
 --EXPECT--
diff --git a/php.ini-development b/php.ini-development
index ee75459ea56..78ae50708d5 100644
--- a/php.ini-development
+++ b/php.ini-development
@@ -1305,10 +1305,9 @@ session.save_handler = files
 ; Strict session mode does not accept an uninitialized session ID, and
 ; regenerates the session ID if the browser sends an uninitialized session ID.
 ; Strict mode protects applications from session fixation via a session adoption
-; vulnerability. It is disabled by default for maximum compatibility, but
-; enabling it is encouraged.
+; vulnerability.
 ; https://wiki.php.net/rfc/strict_sessions
-session.use_strict_mode = 0
+session.use_strict_mode = 1

 ; Whether to use cookies.
 ; https://php.net/session.use-cookies
@@ -1350,13 +1349,13 @@ session.cookie_domain =
 ; Whether or not to add the httpOnly flag to the cookie, which makes it
 ; inaccessible to browser scripting languages such as JavaScript.
 ; https://php.net/session.cookie-httponly
-session.cookie_httponly =
+session.cookie_httponly = 1

 ; Add SameSite attribute to cookie to help mitigate Cross-Site Request Forgery (CSRF/XSRF)
 ; Current valid values are "Strict", "Lax" or "None". When using "None",
 ; make sure to include the quotes, as `none` is interpreted like `false` in ini files.
 ; https://tools.ietf.org/html/draft-west-first-party-cookies-07
-session.cookie_samesite =
+session.cookie_samesite = "Lax"

 ; Handler used to serialize data. php is the standard serializer of PHP.
 ; https://php.net/session.serialize-handler
diff --git a/php.ini-production b/php.ini-production
index b10e2ba9944..eb6880fe75d 100644
--- a/php.ini-production
+++ b/php.ini-production
@@ -1307,10 +1307,9 @@ session.save_handler = files
 ; Strict session mode does not accept an uninitialized session ID, and
 ; regenerates the session ID if the browser sends an uninitialized session ID.
 ; Strict mode protects applications from session fixation via a session adoption
-; vulnerability. It is disabled by default for maximum compatibility, but
-; enabling it is encouraged.
+; vulnerability.
 ; https://wiki.php.net/rfc/strict_sessions
-session.use_strict_mode = 0
+session.use_strict_mode = 1

 ; Whether to use cookies.
 ; https://php.net/session.use-cookies
@@ -1352,13 +1351,13 @@ session.cookie_domain =
 ; Whether or not to add the httpOnly flag to the cookie, which makes it
 ; inaccessible to browser scripting languages such as JavaScript.
 ; https://php.net/session.cookie-httponly
-session.cookie_httponly =
+session.cookie_httponly = 1

 ; Add SameSite attribute to cookie to help mitigate Cross-Site Request Forgery (CSRF/XSRF)
 ; Current valid values are "Strict", "Lax" or "None". When using "None",
 ; make sure to include the quotes, as `none` is interpreted like `false` in ini files.
 ; https://tools.ietf.org/html/draft-west-first-party-cookies-07
-session.cookie_samesite =
+session.cookie_samesite = "Lax"

 ; Handler used to serialize data. php is the standard serializer of PHP.
 ; https://php.net/session.serialize-handler