Commit c130d42be4e for php.net
commit c130d42be4ede3e2c530e43d4212a8b26c3e0533
Author: Jorg Adam Sowa <jorg.sowa@gmail.com>
Date: Sat May 2 16:51:02 2026 +0200
Validate SameSite cookie attribute against allowed values (#21670)
Extract php_is_valid_samesite_value() in ext/standard/head.c as a
shared validation function that enforces the SameSite whitelist
(Strict, Lax, None, or empty string) with case-insensitive matching.
Apply validation in both setcookie()/setrawcookie() (replacing the
existing TODO comment) and the session.cookie_samesite INI handler.
Previously arbitrary strings including CRLF sequences were accepted
and appended verbatim into the Set-Cookie header.
diff --git a/ext/session/session.c b/ext/session/session.c
index 96e32ea7043..0a6ad38e558 100644
--- a/ext/session/session.c
+++ b/ext/session/session.c
@@ -739,6 +739,20 @@ static PHP_INI_MH(OnUpdateSessionStr)
return OnUpdateStr(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage);
}
+static PHP_INI_MH(OnUpdateSessionSameSite)
+{
+ SESSION_CHECK_ACTIVE_STATE;
+ SESSION_CHECK_OUTPUT_STATE;
+
+ if (new_value && ZSTR_LEN(new_value) > 0 && !php_is_valid_samesite_value(new_value)) {
+ php_error_docref(NULL, E_WARNING,
+ "session.cookie_samesite must be \"Strict\", \"Lax\", \"None\", or \"\"");
+ return FAILURE;
+ }
+
+ return OnUpdateStr(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage);
+}
+
static PHP_INI_MH(OnUpdateSessionBool)
{
SESSION_CHECK_ACTIVE_STATE;
@@ -910,7 +924,7 @@ PHP_INI_BEGIN()
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, OnUpdateSessionStr, cookie_samesite, 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.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)
diff --git a/ext/session/tests/session_get_cookie_params_basic.phpt b/ext/session/tests/session_get_cookie_params_basic.phpt
index 73ffd4c94d1..8e7f8da4e3f 100644
--- a/ext/session/tests/session_get_cookie_params_basic.phpt
+++ b/ext/session/tests/session_get_cookie_params_basic.phpt
@@ -30,7 +30,7 @@
"domain" => "baz",
"secure" => FALSE,
"httponly" => FALSE,
- "samesite" => "please"]));
+ "samesite" => "Strict"]));
var_dump(session_get_cookie_params());
var_dump(session_set_cookie_params([
"secure" => TRUE,
@@ -107,7 +107,7 @@
["httponly"]=>
bool(false)
["samesite"]=>
- string(6) "please"
+ string(6) "Strict"
}
bool(true)
array(7) {
@@ -124,6 +124,6 @@
["httponly"]=>
bool(false)
["samesite"]=>
- string(6) "please"
+ string(6) "Strict"
}
Done
diff --git a/ext/session/tests/session_get_cookie_params_variation1.phpt b/ext/session/tests/session_get_cookie_params_variation1.phpt
index 7ce112c9b94..0ab0f233530 100644
--- a/ext/session/tests/session_get_cookie_params_variation1.phpt
+++ b/ext/session/tests/session_get_cookie_params_variation1.phpt
@@ -30,7 +30,7 @@
var_dump(session_get_cookie_params());
ini_set("session.cookie_httponly", TRUE);
var_dump(session_get_cookie_params());
-ini_set("session.cookie_samesite", "foo");
+ini_set("session.cookie_samesite", "Lax");
var_dump(session_get_cookie_params());
ini_set("session.cookie_partitioned", TRUE);
var_dump(session_get_cookie_params());
@@ -150,7 +150,7 @@
["httponly"]=>
bool(true)
["samesite"]=>
- string(3) "foo"
+ string(3) "Lax"
}
array(7) {
["lifetime"]=>
@@ -166,6 +166,6 @@
["httponly"]=>
bool(true)
["samesite"]=>
- string(3) "foo"
+ string(3) "Lax"
}
Done
diff --git a/ext/session/tests/session_set_cookie_params_invalid_ini.phpt b/ext/session/tests/session_set_cookie_params_invalid_ini.phpt
new file mode 100644
index 00000000000..61728c342ea
--- /dev/null
+++ b/ext/session/tests/session_set_cookie_params_invalid_ini.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Test session.cookie_samesite with invalid INI value
+--INI--
+session.cookie_samesite=Invalid
+--EXTENSIONS--
+session
+--SKIPIF--
+<?php include('skipif.inc'); ?>
+--FILE--
+<?php
+
+ob_start();
+
+var_dump(ini_get("session.cookie_samesite"));
+
+echo "Done";
+ob_end_flush();
+?>
+--EXPECTF--
+Warning: PHP Startup: session.cookie_samesite must be "Strict", "Lax", "None", or "" in Unknown on line 0
+string(0) ""
+Done
diff --git a/ext/session/tests/session_set_cookie_params_variation6.phpt b/ext/session/tests/session_set_cookie_params_variation6.phpt
index 61243f82751..fbf958a0be0 100644
--- a/ext/session/tests/session_set_cookie_params_variation6.phpt
+++ b/ext/session/tests/session_set_cookie_params_variation6.phpt
@@ -1,7 +1,7 @@
--TEST--
-Test session_set_cookie_params() function : variation
+Test session_set_cookie_params() samesite validation
--INI--
-session.cookie_samesite=test
+session.cookie_samesite=Lax
--EXTENSIONS--
session
--SKIPIF--
@@ -11,36 +11,56 @@
ob_start();
-echo "*** Testing session_set_cookie_params() : variation ***\n";
-
+echo "-- Valid values --\n";
var_dump(ini_get("session.cookie_samesite"));
-var_dump(session_set_cookie_params(["samesite" => "nothing"]));
+var_dump(session_set_cookie_params(["samesite" => "Strict"]));
var_dump(ini_get("session.cookie_samesite"));
-var_dump(session_start());
+var_dump(session_set_cookie_params(["samesite" => "None"]));
var_dump(ini_get("session.cookie_samesite"));
-var_dump(session_set_cookie_params(["samesite" => "test"]));
+var_dump(session_set_cookie_params(["samesite" => ""]));
var_dump(ini_get("session.cookie_samesite"));
-var_dump(session_destroy());
+
+echo "-- Invalid value via session_set_cookie_params --\n";
+var_dump(session_set_cookie_params(["samesite" => "Invalid"]));
var_dump(ini_get("session.cookie_samesite"));
-var_dump(session_set_cookie_params(["samesite" => "other"]));
+
+echo "-- Invalid value via ini_set --\n";
+var_dump(ini_set("session.cookie_samesite", "Invalid"));
var_dump(ini_get("session.cookie_samesite"));
+echo "-- Cannot change while session is active --\n";
+var_dump(session_set_cookie_params(["samesite" => "Lax"]));
+var_dump(session_start());
+var_dump(session_set_cookie_params(["samesite" => "Strict"]));
+var_dump(session_destroy());
+
echo "Done";
ob_end_flush();
?>
--EXPECTF--
-*** Testing session_set_cookie_params() : variation ***
-string(4) "test"
+-- Valid values --
+string(3) "Lax"
bool(true)
-string(7) "nothing"
+string(6) "Strict"
bool(true)
-string(7) "nothing"
+string(4) "None"
+bool(true)
+string(0) ""
+-- Invalid value via session_set_cookie_params --
-Warning: session_set_cookie_params(): Session cookie parameters cannot be changed when a session is active (started from %s on line %d) in %s on line %d
+Warning: session_set_cookie_params(): session.cookie_samesite must be "Strict", "Lax", "None", or "" in %s on line %d
+bool(false)
+string(0) ""
+-- Invalid value via ini_set --
+
+Warning: ini_set(): session.cookie_samesite must be "Strict", "Lax", "None", or "" in %s on line %d
bool(false)
-string(7) "nothing"
+string(0) ""
+-- Cannot change while session is active --
bool(true)
-string(7) "nothing"
bool(true)
-string(5) "other"
+
+Warning: session_set_cookie_params(): Session cookie parameters cannot be changed when a session is active (started from %s on line %d) in %s on line %d
+bool(false)
+bool(true)
Done
diff --git a/ext/session/tests/session_set_cookie_params_variation7.phpt b/ext/session/tests/session_set_cookie_params_variation7.phpt
index 430c6efc36e..3780fc0222f 100644
--- a/ext/session/tests/session_set_cookie_params_variation7.phpt
+++ b/ext/session/tests/session_set_cookie_params_variation7.phpt
@@ -33,7 +33,7 @@
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(session_set_cookie_params(["secure" => true, "samesite" => "Strict"]));
var_dump(ini_get("session.cookie_secure"));
var_dump(ini_get("session.cookie_samesite"));
@@ -66,7 +66,7 @@
string(0) ""
bool(true)
string(1) "1"
-string(6) "please"
+string(6) "Strict"
string(1) "0"
bool(true)
string(2) "42"
diff --git a/ext/standard/head.c b/ext/standard/head.c
index 69e8d1f794f..03f2e6189ee 100644
--- a/ext/standard/head.c
+++ b/ext/standard/head.c
@@ -74,6 +74,13 @@ PHPAPI bool php_header(void)
}
}
+PHPAPI bool php_is_valid_samesite_value(zend_string *value)
+{
+ return zend_string_equals_literal_ci(value, "Strict")
+ || zend_string_equals_literal_ci(value, "Lax")
+ || zend_string_equals_literal_ci(value, "None");
+}
+
#define ILLEGAL_COOKIE_CHARACTER "\",\", \";\", \" \", \"\\t\", \"\\r\", \"\\n\", \"\\013\", or \"\\014\""
PHPAPI zend_result php_setcookie(zend_string *name, zend_string *value, time_t expires,
zend_string *path, zend_string *domain, bool secure, bool httponly,
@@ -121,7 +128,11 @@ PHPAPI zend_result php_setcookie(zend_string *name, zend_string *value, time_t e
return FAILURE;
}
- /* Should check value of SameSite? */
+ if (samesite && ZSTR_LEN(samesite) > 0 && !php_is_valid_samesite_value(samesite)) {
+ zend_value_error("%s(): \"samesite\" option must be \"Strict\", \"Lax\", \"None\", or \"\"",
+ get_active_function_name());
+ return FAILURE;
+ }
if (value == NULL || ZSTR_LEN(value) == 0) {
/*
diff --git a/ext/standard/head.h b/ext/standard/head.h
index 07c947f8ad0..8b91371a46e 100644
--- a/ext/standard/head.h
+++ b/ext/standard/head.h
@@ -24,6 +24,8 @@
#define COOKIE_SAMESITE "; SameSite="
#define COOKIE_PARTITIONED "; Partitioned"
+PHPAPI bool php_is_valid_samesite_value(zend_string *value);
+
extern PHP_RINIT_FUNCTION(head);
PHPAPI bool php_header(void);
diff --git a/ext/standard/tests/setcookie_samesite_validation.phpt b/ext/standard/tests/setcookie_samesite_validation.phpt
new file mode 100644
index 00000000000..3827f04fe8d
--- /dev/null
+++ b/ext/standard/tests/setcookie_samesite_validation.phpt
@@ -0,0 +1,52 @@
+--TEST--
+setcookie() and setrawcookie() validate samesite option
+--FILE--
+<?php
+ob_start();
+
+// Valid values
+var_dump(setcookie('test', 'value', ['samesite' => 'Strict']));
+var_dump(setcookie('test', 'value', ['samesite' => 'Lax']));
+var_dump(setcookie('test', 'value', ['samesite' => 'None']));
+var_dump(setcookie('test', 'value', ['samesite' => '']));
+
+// Case-insensitive
+var_dump(setcookie('test', 'value', ['samesite' => 'strict']));
+var_dump(setcookie('test', 'value', ['samesite' => 'LAX']));
+var_dump(setcookie('test', 'value', ['samesite' => 'NONE']));
+
+// setrawcookie uses the same validation
+var_dump(setrawcookie('test', 'value', ['samesite' => 'Lax']));
+
+// Invalid values
+try {
+ setcookie('test', 'value', ['samesite' => 'Invalid']);
+} catch (ValueError $e) {
+ echo $e->getMessage() . "\n";
+}
+
+try {
+ setcookie('test', 'value', ['samesite' => "Strict\r\nX-Injected: evil"]);
+} catch (ValueError $e) {
+ echo $e->getMessage() . "\n";
+}
+
+try {
+ setrawcookie('test', 'value', ['samesite' => 'Invalid']);
+} catch (ValueError $e) {
+ echo $e->getMessage() . "\n";
+}
+
+?>
+--EXPECTF--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+setcookie(): "samesite" option must be "Strict", "Lax", "None", or ""
+setcookie(): "samesite" option must be "Strict", "Lax", "None", or ""
+setrawcookie(): "samesite" option must be "Strict", "Lax", "None", or ""