Skip to content

Commit 233c984

Browse files
committed
Add SameSite parameter to cookie setting functions
1 parent a493da7 commit 233c984

11 files changed

+201
-17
lines changed

ext/session/session.c

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include "ext/standard/basic_functions.h"
4747
#include "ext/standard/head.h"
4848
#include "ext/random/php_random.h"
49+
#include "zend_enum.h"
4950

5051
#include "mod_files.h"
5152
#include "mod_user.h"
@@ -1667,6 +1668,7 @@ PHP_FUNCTION(session_set_cookie_params)
16671668
zend_string *lifetime = NULL, *path = NULL, *domain = NULL, *samesite = NULL;
16681669
bool secure = 0, secure_null = 1;
16691670
bool httponly = 0, httponly_null = 1;
1671+
zend_object *same_site_enum = NULL;
16701672
zend_string *ini_name;
16711673
zend_result result;
16721674
int found = 0;
@@ -1675,13 +1677,14 @@ PHP_FUNCTION(session_set_cookie_params)
16751677
return;
16761678
}
16771679

1678-
ZEND_PARSE_PARAMETERS_START(1, 5)
1680+
ZEND_PARSE_PARAMETERS_START(1, 6)
16791681
Z_PARAM_ARRAY_HT_OR_LONG(options_ht, lifetime_long)
16801682
Z_PARAM_OPTIONAL
16811683
Z_PARAM_STR_OR_NULL(path)
16821684
Z_PARAM_STR_OR_NULL(domain)
16831685
Z_PARAM_BOOL_OR_NULL(secure, secure_null)
16841686
Z_PARAM_BOOL_OR_NULL(httponly, httponly_null)
1687+
Z_PARAM_OBJ_OF_CLASS(same_site_enum, SameSite_ce)
16851688
ZEND_PARSE_PARAMETERS_END();
16861689

16871690
if (PS(session_status) == php_session_active) {
@@ -1717,6 +1720,12 @@ PHP_FUNCTION(session_set_cookie_params)
17171720
zend_argument_value_error(5, "must be null when argument #1 ($lifetime_or_options) is an array");
17181721
RETURN_THROWS();
17191722
}
1723+
/* Use ArgumentCount error trick similar to base setcookie() function */
1724+
if (same_site_enum) {
1725+
zend_argument_count_error("session_set_cookie_params(): Expects exactly 1 arguments when argument #1 "
1726+
"($lifetime_or_options) is an array");
1727+
RETURN_THROWS();
1728+
}
17201729
ZEND_HASH_FOREACH_STR_KEY_VAL(options_ht, key, value) {
17211730
if (key) {
17221731
ZVAL_DEREF(value);
@@ -1806,6 +1815,20 @@ PHP_FUNCTION(session_set_cookie_params)
18061815
goto cleanup;
18071816
}
18081817
}
1818+
if (same_site_enum) {
1819+
zval *case_name = zend_enum_fetch_case_name(same_site_enum);
1820+
samesite = zend_string_copy(Z_STR_P(case_name));
1821+
1822+
/* Verify that cookie is secure if using SameSite::None, see
1823+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure */
1824+
if (!secure && zend_string_equals_literal(samesite, "None")) {
1825+
zend_string_release(samesite);
1826+
zend_argument_value_error(6, "can only be SameSite::None if argument #6 ($secure) is true");
1827+
RETVAL_FALSE;
1828+
goto cleanup;
1829+
/* RETURN_THROWS(); */
1830+
}
1831+
}
18091832
if (samesite) {
18101833
ini_name = zend_string_init("session.cookie_samesite", sizeof("session.cookie_samesite") - 1, 0);
18111834
result = zend_alter_ini_entry(ini_name, samesite, PHP_INI_USER, PHP_INI_STAGE_RUNTIME);

ext/session/session.stub.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function session_cache_limiter(?string $value = null): string|false {}
8585

8686
function session_cache_expire(?int $value = null): int|false {}
8787

88-
function session_set_cookie_params(array|int $lifetime_or_options, ?string $path = null, ?string $domain = null, ?bool $secure = null, ?bool $httponly = null): bool {}
88+
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 {}
8989

9090
function session_start(array $options = []): bool {}
9191

ext/session/session_arginfo.h

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
--TEST--
2+
session_set_cookie_params() SameSite parameter tests
3+
--EXTENSIONS--
4+
session
5+
--SKIPIF--
6+
<?php include('skipif.inc'); ?>
7+
--FILE--
8+
<?php
9+
10+
ob_start();
11+
12+
// Check type errors work
13+
try {
14+
session_set_cookie_params(20, sameSite: new stdClass());
15+
} catch (\TypeError $e) {
16+
echo $e->getMessage(), \PHP_EOL;
17+
}
18+
19+
// Check ValueError when using SameSite::None and the cookie is not secure, see
20+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure
21+
try {
22+
session_set_cookie_params(20, sameSite: SameSite::None);
23+
} catch (\ValueError $e) {
24+
echo $e->getMessage(), \PHP_EOL;
25+
}
26+
try {
27+
session_set_cookie_params(20, sameSite: SameSite::None, secure: false);
28+
} catch (\ValueError $e) {
29+
echo $e->getMessage(), \PHP_EOL;
30+
}
31+
32+
// Check get argument count error when using an option array
33+
try {
34+
session_set_cookie_params(['secure' => false], sameSite: SameSite::None);
35+
} catch (\ArgumentCountError $e) {
36+
echo $e->getMessage(), \PHP_EOL;
37+
}
38+
39+
echo "*** Testing session_set_cookie_params(20, sameSite:) ***\n";
40+
var_dump(session_set_cookie_params(20, sameSite: SameSite::None, secure: true));
41+
var_dump(ini_get("session.cookie_samesite"));
42+
var_dump(session_set_cookie_params(20, sameSite: SameSite::Lax, secure: true));
43+
var_dump(ini_get("session.cookie_samesite"));
44+
var_dump(session_set_cookie_params(20, sameSite: SameSite::Strict, secure: true));
45+
var_dump(ini_get("session.cookie_samesite"));
46+
// After session started
47+
var_dump(session_start());
48+
var_dump(session_set_cookie_params(20, sameSite: SameSite::None, secure: true));
49+
var_dump(ini_get("session.cookie_samesite"));
50+
var_dump(session_set_cookie_params(20, sameSite: SameSite::Lax, secure: true));
51+
var_dump(ini_get("session.cookie_samesite"));
52+
var_dump(session_set_cookie_params(20, sameSite: SameSite::Strict, secure: true));
53+
54+
echo "Done";
55+
ob_end_flush();
56+
?>
57+
--EXPECTF--
58+
session_set_cookie_params(): Argument #6 ($sameSite) must be of type SameSite, stdClass given
59+
session_set_cookie_params(): Argument #6 ($sameSite) can only be SameSite::None if argument #6 ($secure) is true
60+
session_set_cookie_params(): Argument #6 ($sameSite) can only be SameSite::None if argument #6 ($secure) is true
61+
session_set_cookie_params(): Expects exactly 1 arguments when argument #1 ($lifetime_or_options) is an array
62+
*** Testing session_set_cookie_params(20, sameSite:) ***
63+
bool(true)
64+
string(4) "None"
65+
bool(true)
66+
string(3) "Lax"
67+
bool(true)
68+
string(6) "Strict"
69+
bool(true)
70+
71+
Warning: session_set_cookie_params(): Session cookie parameters cannot be changed when a session is active in %s on line %d
72+
bool(false)
73+
string(6) "Strict"
74+
75+
Warning: session_set_cookie_params(): Session cookie parameters cannot be changed when a session is active in %s on line %d
76+
bool(false)
77+
string(6) "Strict"
78+
79+
Warning: session_set_cookie_params(): Session cookie parameters cannot be changed when a session is active in %s on line %d
80+
bool(false)
81+
Done

ext/standard/basic_functions.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "php_ext_syslog.h"
3333
#include "ext/standard/info.h"
3434
#include "ext/session/php_session.h"
35+
#include "zend_enum.h"
3536
#include "zend_exceptions.h"
3637
#include "zend_attributes.h"
3738
#include "zend_ini.h"
@@ -306,6 +307,7 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */
306307
php_register_incomplete_class_handlers();
307308

308309
assertion_error_ce = register_class_AssertionError(zend_ce_error);
310+
SameSite_ce = register_class_SameSite();
309311

310312
BASIC_MINIT_SUBMODULE(var)
311313
BASIC_MINIT_SUBMODULE(file)

ext/standard/basic_functions.stub.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,9 +2191,15 @@ function header(string $header, bool $replace = true, int $response_code = 0): v
21912191

21922192
function header_remove(?string $name = null): void {}
21932193

2194-
function setrawcookie(string $name, string $value = "", array|int $expires_or_options = 0, string $path = "", string $domain = "", bool $secure = false, bool $httponly = false): bool {}
2194+
enum SameSite {
2195+
case None;
2196+
case Lax;
2197+
case Strict;
2198+
}
2199+
2200+
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 {}
21952201

2196-
function setcookie(string $name, string $value = "", array|int $expires_or_options = 0, string $path = "", string $domain = "", bool $secure = false, bool $httponly = false): bool {}
2202+
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 {}
21972203

21982204
function http_response_code(int $response_code = 0): int|bool {}
21992205

ext/standard/basic_functions_arginfo.h

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/standard/head.c

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525

2626
#include "php_globals.h"
2727
#include "zend_smart_str.h"
28+
#include "zend_enum.h"
2829

30+
PHPAPI zend_class_entry *SameSite_ce;
2931

3032
/* Implementation of the language Header() function */
3133
/* {{{ Sends a raw HTTP header */
@@ -227,9 +229,10 @@ static void php_setcookie_common(INTERNAL_FUNCTION_PARAMETERS, bool is_raw)
227229
HashTable *options = NULL;
228230
zend_long expires = 0;
229231
zend_string *name, *value = NULL, *path = NULL, *domain = NULL, *samesite = NULL;
232+
zend_object *same_site_enum = NULL;
230233
bool secure = 0, httponly = 0;
231234

232-
ZEND_PARSE_PARAMETERS_START(1, 7)
235+
ZEND_PARSE_PARAMETERS_START(1, 8)
233236
Z_PARAM_STR(name)
234237
Z_PARAM_OPTIONAL
235238
Z_PARAM_STR(value)
@@ -238,6 +241,7 @@ static void php_setcookie_common(INTERNAL_FUNCTION_PARAMETERS, bool is_raw)
238241
Z_PARAM_STR(domain)
239242
Z_PARAM_BOOL(secure)
240243
Z_PARAM_BOOL(httponly)
244+
Z_PARAM_OBJ_OF_CLASS(same_site_enum, SameSite_ce)
241245
ZEND_PARSE_PARAMETERS_END();
242246

243247
if (options) {
@@ -254,6 +258,19 @@ static void php_setcookie_common(INTERNAL_FUNCTION_PARAMETERS, bool is_raw)
254258
}
255259
}
256260

261+
if (same_site_enum) {
262+
zval *case_name = zend_enum_fetch_case_name(same_site_enum);
263+
samesite = zend_string_copy(Z_STR_P(case_name));
264+
265+
/* Verify that cookie is secure if using SameSite::None, see
266+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure */
267+
if (!secure && zend_string_equals_literal(samesite, "None")) {
268+
zend_string_release(samesite);
269+
zend_argument_value_error(8, "can only be SameSite::None if argument #6 ($secure) is true");
270+
RETURN_THROWS();
271+
}
272+
}
273+
257274
if (php_setcookie(name, value, expires, path, domain, secure, httponly, samesite, !is_raw) == SUCCESS) {
258275
RETVAL_TRUE;
259276
} else {

ext/standard/head.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,6 @@ PHPAPI zend_result php_setcookie(zend_string *name, zend_string *value, time_t e
3232
zend_string *path, zend_string *domain, bool secure, bool httponly,
3333
zend_string *samesite, bool url_encode);
3434

35+
extern PHPAPI zend_class_entry *SameSite_ce;
36+
3537
#endif

ext/standard/tests/network/setcookie.phpt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ setcookie('name', 'value', 0, '', '', FALSE, TRUE);
1919

2020
setcookie('name', 'value', ['expires' => $tsp]);
2121
setcookie('name', 'value', ['expires' => $tsn, 'path' => '/path/', 'domain' => 'domain.tld', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict']);
22+
setcookie('name', 'value', /*expires:*/ $tsp, path: '/path/', domain: 'domain.tld', secure: true, httponly: true, sameSite: SameSite::None);
23+
setcookie('name', 'value', /*expires:*/ $tsp, path: '/path/', domain: 'domain.tld', secure: true, httponly: true, sameSite: SameSite::Lax);
24+
setcookie('name', 'value', /*expires:*/ $tsp, path: '/path/', domain: 'domain.tld', secure: true, httponly: true, sameSite: SameSite::Strict);
2225

2326
$expected = array(
2427
'Set-Cookie: name=deleted; expires='.date('D, d M Y H:i:s', 1).' GMT; Max-Age=0',
@@ -34,7 +37,10 @@ $expected = array(
3437
'Set-Cookie: name=value; secure',
3538
'Set-Cookie: name=value; HttpOnly',
3639
'Set-Cookie: name=value; expires='.date('D, d M Y H:i:s', $tsp).' GMT; Max-Age=5',
37-
'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'
40+
'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',
41+
'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',
42+
'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',
43+
'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',
3844
);
3945

4046
$headers = headers_list();
@@ -44,26 +50,20 @@ if (($i = count($expected)) > count($headers))
4450
return;
4551
}
4652

47-
do {
48-
$header = current($headers);
49-
if (strncmp($header, 'Set-Cookie:', 11) !== 0) {
53+
foreach ($headers as $header) {
54+
if (!str_starts_with($header, 'Set-Cookie:')) {
5055
continue;
5156
}
52-
5357
// If the second rolls over between the time() call and the internal time determination by
5458
// setcookie(), we might get Max-Age=4 instead of Max-Age=5.
5559
$header = str_replace('Max-Age=4', 'Max-Age=5', $header);
5660
if ($header === current($expected)) {
5761
$i--;
5862
} else {
59-
echo "Header mismatch:\n\tExpected: "
60-
.current($expected)
61-
."\n\tReceived: ".current($headers)."\n";
63+
echo "Header mismatch:\n\tExpected: ", current($expected), "\n\tReceived: ", $header, "\n";
6264
}
63-
6465
next($expected);
6566
}
66-
while (next($headers) !== FALSE);
6767

6868
echo ($i === 0)
6969
? 'OK'
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
setcookie() SameSite parameter errors
3+
--FILE--
4+
<?php
5+
6+
// Check type errors work
7+
try {
8+
setcookie('name', sameSite: new stdClass());
9+
} catch (\TypeError $e) {
10+
echo $e->getMessage(), \PHP_EOL;
11+
}
12+
13+
// Check ValueError when using SameSite::None and the cookie is not secure, see
14+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure
15+
try {
16+
setcookie('name', sameSite: SameSite::None);
17+
} catch (\ValueError $e) {
18+
echo $e->getMessage(), \PHP_EOL;
19+
}
20+
try {
21+
setcookie('name', sameSite: SameSite::None, secure: false);
22+
} catch (\ValueError $e) {
23+
echo $e->getMessage(), \PHP_EOL;
24+
}
25+
26+
// Sanity enum check
27+
var_dump(SameSite::Strict);
28+
?>
29+
--EXPECT--
30+
setcookie(): Argument #8 ($sameSite) must be of type SameSite, stdClass given
31+
setcookie(): Argument #8 ($sameSite) can only be SameSite::None if argument #6 ($secure) is true
32+
setcookie(): Argument #8 ($sameSite) can only be SameSite::None if argument #6 ($secure) is true
33+
enum(SameSite::Strict)

0 commit comments

Comments
 (0)