Secure by default set-cookie functions in PHP

Recently I studied the upcoming changes related to treating the SameSite cookie attribute.
And when I’ve got to the respective RFC, proposing a new parameter to the setcookie function, I was disappointed twice.

The reason to that was the decision taken and the cause of this decision. While I completely understand the historical and cultural base for this decision, I still discourage you to take it as an example solution for your own coding problems.

So the proposal was: add another parameter to the setcookie function for the ‘SameSite’ flag that was introduced recently. This would be the eights parameter to that function, that already sounds horrifying, but still it was in a “spirit” of this function.

However the proposal was declined by the voters in favor of another solution, which I would describe as following:

Let us stop adding parameters to this function, as there are already plenty of them
 and we don't know how many will appear in the future. We need a more flexible interface for this.

Of course, when it comes to flexibility in PHP the Array comes into the game. So the voted alternative signature became:

1
setcookie ( string $name [, string $value = "" [, array $options = [] ]] ) : bool

Hurray, now we can pass everything we want! But wait, what can we pass? What are the name of keys in the $options array?
Is it ‘expire’ or ‘expires’? Is it ‘httponly’ or ‘HttpOnly’ or maybe ‘http_only? ‘SameSite’ or ‘samesite’? Can I pass ‘maxage’?
You will always need to go to php.net to answer these questions. So the interface became absolutely unclear.

Okay, it’s not an easy thing to support and update a programming language (especially PHP), so let us not throw stones in the voters.
Let us instead learn what the problem is and how we can do better.

Let’s take a look at the setcookie function arguments:

1
2
3
4
5
6
7
8
9
10
setcookie (
string $name,
string $value = "",
int $expires = 0,
string $path = "",
string $domain = "",
bool $secure = false,
bool $httponly = false,
string $samesite = ""
)

The first two, name and value, are the attributes of the cookie itself. The last six are instruction to a browser on how this cookie has to be managed.

First step would be to introduce a Cookie value object with $name and $value constructor arguments:

1
2
3
4
5
6
7
8
9
setcookie (
Cookie $cookie,
int $expires = 0,
string $path = "",
string $domain = "",
bool $secure = false,
bool $httponly = false,
string $samesite = ""
)

The $expires parameter indicates the maximum lifetime of the cookie, represented as the timestamp of the date and time at which the cookie expires. The default value, 0, means that expiration date is not set for the cookie, so the browser keeps it for the session lifetime.

Most of the time you will find yourself writing something like: now() + 604800 /* one week */ for this parameter. Of course, we want to use a DateTime value object for this as well:

1
2
3
4
5
6
7
8
9
setcookie (
Cookie $cookie,
DateTime $expirationDate,
string $path = "",
string $domain = "",
bool $secure = false,
bool $httponly = false,
string $samesite = ""
)

As for the path and domain we could also introduce value objects, but there is no necessity for that. Let’s keep it simple and just stick with strings. We also leave the default values out of scope for now, otherwise we will need to dig deep into the HTTP State Management Mechanism RFC.

However we change the order of the arguments to a more natural one:

1
2
3
4
5
6
7
8
9
setcookie (
Cookie $cookie,
DateTime $expires,
string $domain = "",
string $path = "",
bool $secure = false,
bool $httponly = false,
string $samesite = ""
)

Now let’s handle the rest of the arguments.
First of all, you can see that the function is not secure by default. These relates to all three arguments, because they all are about security.
Second, these attributes are all just flags. Flag arguments reduce the readability and hide the intention of the function. Take a look at the example below.

1
setcookie(new Cookie('sause', 'bbq'), new DateTime('+1 week'), 'example.com', '/', true, false, 'Strict');

What are these true, false? Do you remember the order of the arguments?
What are other options for the samesite argument? What if I accidentally pass 'Steict' instead of 'Strict'?
What does the default value "" mean for the $samesite argument? Questions, questions. Is there a way to answer them all at once?

There is: remove the flag arguments and create a separate function for each meaningful state. Here you will end up with 10 functions:

1
2
3
4
5
6
7
8
9
10
setSameSiteCookie(Cookie $cookie, DateTime $expires, string $domain = "", string $path = "");
setLaxSameSiteCookie(Cookie $cookie, DateTime $expires, string $domain = "", string $path = "");
setNotSameSiteCookie(Cookie $cookie, DateTime $expires, string $domain = "", string $path = "");
setSameSiteNotHttpOnlyCookie(Cookie $cookie, DateTime $expires, string $domain = "", string $path = "");
setLaxSameSiteNotHttpOnlyCookie(Cookie $cookie, DateTime $expires, string $domain = "", string $path = "");
setNotSameSiteNotHttpOnlyCookie(Cookie $cookie, DateTime $expires, string $domain = "", string $path = "");
setSameSiteNotSecureCookie(Cookie $cookie, DateTime $expires, string $domain = "", string $path = "");
setLaxSameSiteNotSecureCookie(Cookie $cookie, DateTime $expires, string $domain = "", string $path = "");
setSameSiteNotSecureNotHttpOnlyCookie(Cookie $cookie, DateTime $expires, string $domain = "", string $path = "");
setLaxSameSiteNotSecureNotHttpOnlyCookie(Cookie $cookie, DateTime $expires, string $domain = "", string $path = "");

As of the mentioned changes cookies with SameSite=None must be also with Secure flag, otherwise they are ignored, we can skip “not-same-site, not-secure” combinations.

Now I will just give you some examples, so you can compare their readability by yourselves:

1
2
3
setcookie('sause', 'bbq', now() + 60 * 60 * 24 * 7, '/', 'example.com', true, true, 'Lax');
// vs
setLaxSameSiteCookie(new Cookie('sause', 'bbq'), new DateTime('+1 week'), 'example.com', '/');
1
2
3
setcookie('sause', 'bbq', strtotime('+1 month'), '/', 'example.com', true, false, 'Strict');
// vs
setSameSiteNotHttpOnlyCookie(new Cookie('sause', 'bbq'), new DateTime('+1 month'), 'example.com', '/');

And this one is my favorite:

1
2
3
setcookie('sause', 'bbq', now() + 1209600, '/', 'example.com');
// vs
setLaxSameSiteNotSecureNotHttpOnlyCookie(new Cookie('sause', 'bbq'), new DateTime('+2 weeks'), 'example.com', '/');

The first variant seems so short and easy, yet it hides all the insecurity and uncertainty. It’s so easy to make a mistake there, whereas it is really difficult to do so in the second variant.

The approach to building interfaces described here helps keeping the surprise level at a very minimum, thus reduces number of bugs and improves code reading time for your colleagues.