Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Murpha's avatar

PHP server bug may cause Laravel session persistence issues

Hey all,

Some may have already known this, but I will post this here in case it helps save someone countless hours/days.

I’ve been developing apps on Laravel since the early days of version 5, but have only just come across this issue recently when using the built-in PHP server – often run using the php artisan serve console command. A change in hardware/software configuration of the system may have provoked the issue, but I was able to replicate, even after resetting Windows, reinstalling the entire dev environment stack and dependencies, etc. It did not occur always occur on other systems and as such, was hard to narrow down – so I’ll go through the steps taken to discover and reproduce the issue.

Environment:

  • Laravel 5.4.30
  • PHP 7.1.17
  • Composer 1.4.2
  • Node 8.2.1

Dependencies (PHP/JS)

  • Default for Laravel clean-slate 5.4.30

Browsers:

  • Firefox 54.0.1
  • Chrome 59.0.3071.115
  • Edge 40.15063

The issue:

I first noticed a bug when developing a Laravel/Vue SPA which would, after successfully logging in and creating a session, cause AJAX consecutive requests to eventually return 401 Unauthenticated. Without immediately being able to solve the issue, I quickly created a clean-slate Laravel project, generate the default authentication scaffolding, took the Example.vue, added a function to send a ‘dummy’ GET request on press the of a button, etc, then started spamming the button. Sometimes instantaneously, other times it took over 10 requests, but sure enough, the request would return a 401. Laravel had killed the session. This lead me down the path of trying the different session drivers – file, database (my go to default) and redis, thinking that perhaps the session was experiencing a race-condition, but this didn’t solve it either.

Next, I decided to investigate CSRF and disabled VerifyCsrf middleware. Oddly, this helped mitigate the issue, which I could not understand as Laravel does not attempt to verify CSRF tokens for read request verbs (GET, HEAD, OPTIONS). So reenabling the CSRF middleware and the axios CSRF header code found in bootstrap.js, I began to investigate the StartSession middleware , particularly the getSession() function and the relevant cookie processing. Through a series of logging and carefully watching requests, I noticed that the Laravel session cookie, in this case the default laravel_session cookie, was being truncated. Laravel’s decrypt() function within \Illuminate\Encryption\Encrypter.php would attempt to verify the base64-encoded JSON payload prior to decrypting it, throwing a DecryptException error due to the malformed JSON.

Now it was time to found out where and why it was being truncated. Watching the Set-cookie and Cookie headers between request, manually pulling out the laravel_session cookie’s payload, and then running it through base64_decode() would reveal a correctly formed JSON payload – so the browser wasn’t the culprit. Digging deeper into Laravel’s cookie parsing, I noticed that, on the first failed request, the laravel_session cookie was return as null using the ParameterBag get() function. PHP’s global $_COOKIE variable would show the laravel_session as being present, but this time when running the payload through base64_decode(), it would show a malformed JSON object. With this I could safely rule out any wrong doing on Laravel’s behalf.

During the early stages debugging, I had noticed that even increasing the amount of parameters and/or parameter length with the GET request would also lead to Error 401 being returned, so I began searching for issues with PHP and header parsing, max lengths, etc. Sure enough, I came across a bug with the built-in PHP server discovered in 2015 - bug #70470. This bug causes PHP’s server to truncate headers spanning over multiple TCP packets. This explains why some requests would work for several consecutive attempts, with others would drop immediately – it was a matter of timing.

As a test, I disabled all CSRF to remove the extra two headers X-CSRF-TOKEN and X-XSRF-TOKEN (which always proceeded the Cookie header in a request), and instead spam several cookies in their place. While logging cookies using the Parameterbag get() function with getSession() over several requests, it was shown that occasionally sometimes 2 or 3 cookies would not be present in the log. Moving over to IIS the log shows that Laravel will consistently receive all cookies, regardless of how much and how often the request was spammed. So if you are experiencing this issue, or perhaps a very similar one, try moving away from the built-in PHP server and towards Apache or IIS.

--- Example ---

Payload in Cookie header as seen on browser side:

eyJpdiI6IlJDVVwvVlBQbGt...4OWNjYmIwZDUzOGQ2NTEifQ==

Base64 decoded payload:

"{"iv":"RCU\/VPPl...45d1ebe33089ccbb0d538d651"}"

Payload as dumped from $_COOKIE:

eyJpdiI6IlJDVVwvVlBQbGt...4OWNjYmIwZDUzOGQ2NTE

Base64 decoded payload:

"{"iv":"RCU\/VPPl...45d1ebe33089ccbb0d538d651"

Note: I have truncated the center of the payloads in the examples above.

As you can see, the end of the payload dumped from the $_COOKIE global variable is missing a few characters when compared to the payload sent via the browser in the Cookie header. This results in a malformed JSON objects and Laravel throws an exception and kills the session. Subsequent requests will return a 401 Unauthenticated error.

Hope this helps someone and thanks for reading.

tldr; PHP’s built-in server suffers from a bug which causes it to truncate request headers if they span over multiple TCP packets. This can caused requests to fail, usually with a 401, as the Laravel session cookie or another header critical to your app, has been truncated. Laravel will kill the session and roll a new one as a result. You can read up on bug #70470 here.

0 likes
2 replies
MidSilence's avatar

Hello,

I just sign up just to thank you about this post. I've the same problem using ngnix/php-cgi and moving to IIS solves the problem.

Further investigation shows me that the default cgi communication between IIS and PHP is NAMEDPIPES not TCP.

Maybe changing to TCP php-cgi will truncate the header as well. I'll try later.

Again, thank you.

Regards,

1 like

Please or to participate in this conversation.